acorn-schema 0.4.0

Portable ACORN schema, validation, and codecs
#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
use super::super::*;
use crate::research_activity::ResearchActivityMetadata;
use crate::validation::{rules, Validate};

const VALID_ARK_VALUES: [&str; 8] = [
    "https://n2t.net/ark:99166/w66d60p2",
    "https://n2t.net/ark:/99166/w66d60p2",
    "ark:1234/w5678",
    "https://n2t.net/ark:12148/btv1b8449691v/f29",
    "https://n2t.net/ark:12148/btv1b8449691v/f29/abc.png",
    "ark:12148/btv1b8449691v/f29/abc.TIFF",
    "https://example.org/ark:12345/x6np1wh8k/c3/s5.v7.xsl",
    "ark:/12148/btv1b8449691v",
];
const VALID_DOI_VALUES: [&str; 31] = [
    "10.11578/dc.20250604.1",
    "10.1000/182",
    "10.97812345/99990",
    "https://doi.org/10.11578/dc.20250604.1",
    "https://doi.org/10.1000/182",
    "https://doi.org/10.97812345/99990",
    "10.1038/s41562-018-0399-z",
    "10.1038/533452a",
    "10.1109/eScience51609.2021.00010",
    "10.1109/ACCESS.2025.3542334",
    "10.1016/j.jbi.2008.04.010",
    "10.48550/arXiv.2312.10997",
    "10.1098/rsos.171511",
    "10.3233/ISU-2010-0613",
    "10.48550/arXiv.2411.06237",
    "10.2139/ssrn.4900122",
    "10.18653/v1/2025.naacl-long.243",
    "10.1007/978-3-642-38288-8_33",
    "10.48550/arXiv.2501.07391",
    "10.48550/arXiv.2504.01990",
    "10.1109/I-SMAC61858.2024.10714814",
    "10.48550/arXiv.2506.06576",
    "10.1186/s13326-025-00327-4",
    "10.1162/dint_a_00186",
    "10.1609/aaai.v39i24.34743",
    "10.48550/arXiv.2504.16736",
    "10.1002/asi.22636",
    "10.1093/gigascience/giy023",
    "10.1162/99608f92.e1f349c2",
    "10.5479/10088/113528",
    "10.1038/s41597-020-0486-7",
];
const VALID_ORCID_VALUES: [&str; 14] = [
    "https://orcid.org/0000-0002-2057-9115",
    "https://orcid.org/0009-0005-5568-6526",
    "0000-0003-1485-2741",
    "0009-0005-8526-4332",
    "0009-0001-1431-2393",
    "0009-0007-2591-8394",
    "0009-0006-8870-3625",
    "0009-0004-4438-9406",
    "0009-0003-1201-0767",
    "0000-0002-2845-8668",
    "0000-0002-0014-1319",
    "0000-0001-9034-3389",
    "0000-0002-0065-494X",
    "0000-0002-2816-415X",
];
const VALID_ROR_VALUES: [&str; 28] = [
    "https://ror.org/01qz5mb56",
    "01qz5mb56",
    "https://ror.org/04ttjf776",
    "https://ror.org/01rxfrp27",
    "https://ror.org/023q4bk22",
    "https://ror.org/006jxzx88",
    "https://ror.org/00wfvh315",
    "https://ror.org/05ktbsm52",
    "https://ror.org/00nx6aa03",
    "https://ror.org/02k3cxs74",
    "https://ror.org/046fa4y88",
    "https://ror.org/02d439m40",
    "https://ror.org/04h08p482",
    "https://ror.org/01zctcs90",
    "https://ror.org/05m7zw681",
    "https://ror.org/03awtex73",
    "https://ror.org/00kv9pj15",
    "https://ror.org/03mjtdk61",
    "https://ror.org/04yyp8h20",
    "https://ror.org/022rkxt86",
    "https://ror.org/01wddqe20",
    "https://ror.org/03kwrfk72",
    "https://ror.org/037mpqg03",
    "https://ror.org/05cggb038",
    "https://ror.org/050b31k83",
    "https://ror.org/01q8f6705",
    "https://ror.org/03t40cz74",
    "https://ror.org/03ebg0v16",
];

#[test]
fn test_country_rule() {
    ["US", "FR", "DE", "GB", "JP", "us", "fr", "de", "gb", "jp"]
        .into_iter()
        .for_each(|value| assert!(rules::country(value).is_ok(), "expected valid country: {value}"));
    ["USA", "U", "U.S", "US1", "U-S", "XX", "ZZ", "", "USa "]
        .into_iter()
        .for_each(|value| assert!(rules::country(value).is_err(), "expected invalid country: {value}"));
}
#[test]
fn test_date_validation() {
    ["2000-01-01", "2025-06-04", "1950-12-28"]
        .into_iter()
        .for_each(|value| assert!(rules::date(value).is_ok(), "expected valid date: {value}"));
    ["1949-01-02", "01-02-2023", "1234", "foo", "42", "2025/06/04"]
        .into_iter()
        .for_each(|value| assert!(rules::date(value).is_err(), "expected invalid date: {value}"));
    ["2000", "2025", "1950", "0999"]
        .into_iter()
        .for_each(|value| assert!(rules::year(value).is_ok(), "expected valid year: {value}"));
    ["3000", "999", "42"]
        .into_iter()
        .for_each(|value| assert!(rules::year(value).is_err(), "expected invalid year: {value}"));
}
#[test]
fn test_format_phone_number() {
    ["555-123-4567", "555.123.4567", "(555) 123-4567"]
        .into_iter()
        .for_each(|value| assert_eq!(format_phone_number(value), Ok("555.123.4567".to_string())));
    ["+1 (555) 123-4567", "+1 (555) 123.4567", "+15551234567"]
        .into_iter()
        .for_each(|value| assert_eq!(format_phone_number(value), Ok("+1.555.123.4567".to_string())));
}
#[test]
fn test_format_timestamp() {
    assert_eq!(
        format_timestamp("2026-03-10T15:37:24.355614500+00:00"),
        "2026-03-10T15:37:24.355614+00:00"
    );
    assert_eq!(format_timestamp("2023-01-01T12:00:00.123456789Z"), "2023-01-01T12:00:00.123456+00:00");
    assert_eq!(format_timestamp("2026-03-10T15:37:24.5-04:30"), "2026-03-10T15:37:24.500000-04:30");
    assert_eq!(format_timestamp("not-a-date"), "not-a-date");
}
#[test]
fn test_has_image_extension() {
    [
        "foo.png",
        "bar.jpg",
        "baz.jpeg",
        "qux.svg",
        "https://example.com/foo.PNG",
        "https://example.com/bar.JPEG",
    ]
    .into_iter()
    .for_each(|value| assert!(has_image_extension(value).is_ok(), "expected image extension: {value}"));
    ["foo.pNg", "bar.jpx", "qux.sVG", "https://example.com/fooPNG"]
        .into_iter()
        .for_each(|value| assert!(has_image_extension(value).is_err(), "expected invalid image extension: {value}"));
}
#[test]
fn test_is_ark() {
    VALID_ARK_VALUES
        .into_iter()
        .for_each(|value| assert!(rules::ark(value).is_ok(), "expected valid ARK: {value}"));
    [
        "978-12345-99990",
        "http://n2t.net/ark:99166/w66d60p2",
        "https://n2t.net/99166/w66d60p2",
        "https://n2t.net/99166/9w66d60p2",
        "https://n2t.net/99166/lw66d60p2",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::ark(value).is_err(), "expected invalid ARK: {value}"));
    assert_eq!(
        rules::ark("https://n2t.net/99166/w66d60p2")
            .expect_err("invalid ARK should fail")
            .to_string(),
        "Provide valid ARK value"
    );
}
#[test]
fn test_is_commit() {
    [
        "a1b2c3d",
        "A1B2C3D4",
        "0123456789abcdef0123456789abcdef01234567",
        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::commit(value).is_ok(), "expected valid commit: {value}"));
    [
        "",
        "123456",
        "xyzxyzx",
        "g123456",
        "123456 ",
        "12345-678",
        "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef0",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::commit(value).is_err(), "expected invalid commit: {value}"));
}
#[test]
fn test_is_doi() {
    VALID_DOI_VALUES
        .into_iter()
        .for_each(|value| assert!(rules::doi(value).is_ok(), "expected valid DOI: {value}"));
    [
        "978-12345-99990",
        "11.11578/dc.20250604.1",
        "<geo coords=\"10.4515260,51.1656910\"></geo>",
        "10.5555/182",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::doi(value).is_err(), "expected invalid DOI: {value}"));
}
#[test]
fn test_is_handle() {
    ["10013/epic.10033", "hdl:20.5000/example", "https://hdl.handle.net/10013/example"]
        .into_iter()
        .for_each(|value| assert!(rules::handle(value).is_ok(), "expected valid Handle: {value}"));
    ["", "docs/index.md", "https://example.org/10013/example"]
        .into_iter()
        .for_each(|value| assert!(rules::handle(value).is_err(), "expected invalid Handle: {value}"));
}
#[test]
fn test_is_iso_639_language_code_validation() {
    ["en", "EN", "en-US"]
        .into_iter()
        .for_each(|value| assert!(is_iso_639_language_code(value).is_ok()));
    ["eng", "zz-ZZ"]
        .into_iter()
        .for_each(|value| assert!(is_iso_639_language_code(value).is_err()));
    ["en", "EN"]
        .into_iter()
        .for_each(|value| assert!(is_iso_639_1_language_code(value).is_ok()));
    ["eng", "zz"]
        .into_iter()
        .for_each(|value| assert!(is_iso_639_1_language_code(value).is_err()));
}
#[test]
fn test_is_kebabcase() {
    ["this-is-valid", "thisisvalid"]
        .into_iter()
        .for_each(|value| assert!(rules::kebabcase(value).is_ok()));
    ["this_is_not_valid", "ThisIsNotValid", "this is not valid", "This-Is-Not-Valid"]
        .into_iter()
        .for_each(|value| assert!(rules::kebabcase(value).is_err()));
}
#[test]
fn test_is_latlon() {
    [-90.0, -45.7, 0.0, 10.0, 90.0]
        .into_iter()
        .for_each(|value| assert!(rules::latitude(value).is_ok()));
    [-90.5, 90.5].into_iter().for_each(|value| assert!(rules::latitude(value).is_err()));
    [-180.0, -90.0, 0.0, 90.0, 180.0]
        .into_iter()
        .for_each(|value| assert!(rules::longitude(value).is_ok()));
    [-180.5, 180.5].into_iter().for_each(|value| assert!(rules::longitude(value).is_err()));
}
#[test]
fn test_is_license() {
    ["MIT", "MIT License", "Not-A-Real-License"]
        .into_iter()
        .for_each(|value| assert!(is_license(value).is_ok()));
    assert!(is_license("").is_err());
}
#[test]
fn test_is_license_invalid_database_path() {
    assert!(is_license("MIT").is_ok());
    assert_eq!(is_license(" ").expect_err("empty license should fail").code, "license");
}
#[test]
fn test_is_license_missing_database_path_env() {
    assert!(is_license("MIT").is_ok());
}
#[test]
fn test_is_orcid() {
    VALID_ORCID_VALUES
        .into_iter()
        .for_each(|value| assert!(rules::orcid(value).is_ok(), "expected valid ORCID: {value}"));
    [
        "0000-0002-1823-1234",
        "https://orcid.org/0000-0002-1823-12345",
        "https://orcid.com/0000-0002-1823-1234",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::orcid(value).is_err(), "expected invalid ORCID: {value}"));
}
#[test]
fn test_is_phone_number() {
    [
        "555-123-4567",
        "555.123.4567",
        "+1 (555) 123-4567",
        "+1 (555) 123.4567",
        "+15551234567",
        "(555) 123-4567",
    ]
    .into_iter()
    .for_each(|value| assert!(rules::phone(value).is_ok(), "expected valid phone: {value}"));
    ["42", "1 (555) 123-4567", "1234-567", "123-4567", "555.555.5555", "555-555-5555"]
        .into_iter()
        .for_each(|value| assert!(rules::phone(value).is_err(), "expected invalid phone: {value}"));
}
#[test]
fn test_is_programming_language() {
    ["Rust", "Not A Language"]
        .into_iter()
        .for_each(|value| assert!(is_programming_language(value).is_ok()));
    assert!(is_programming_language("").is_err());
}
#[test]
fn test_is_programming_language_missing_database_path_env() {
    assert!(is_programming_language("Rust").is_ok());
}
#[test]
fn test_is_ror() {
    VALID_ROR_VALUES
        .into_iter()
        .for_each(|value| assert!(rules::ror(value).is_ok(), "expected valid ROR: {value}"));
    ["978-12345-99990", "10.1000/182", "3t40cz74", "03ebg0v16-"]
        .into_iter()
        .for_each(|value| assert!(rules::ror(value).is_err(), "expected invalid ROR: {value}"));
}
#[test]
fn test_is_semantic_version() {
    ["1", "1.2", "1.2.3", "0.0.0", "999.10.42"]
        .into_iter()
        .for_each(|value| assert!(rules::version(value).is_ok()));
    ["v1.2.3", "1.2.3.4", "alpha", "1.2.x", "2.1.0-alpha.1", "1.0.0+build.42"]
        .into_iter()
        .for_each(|value| assert!(rules::version(value).is_err()));
}
#[test]
fn test_isni_and_gnd_validation() {
    assert!(rules::isni("https://isni.org/isni/0000000492299539").is_ok());
    assert!(rules::isni("0000 0004 9229 9539").is_ok());
    assert!(rules::isni("0000000492299538").is_err());
    assert!(is_gnd("118540238").is_ok());
    assert!(is_gnd("gnd:118540238").is_ok());
    assert!(is_gnd("not-a-gnd").is_err());
}
#[test]
fn test_metadata_ror_list_validation() {
    let valid = ResearchActivityMetadata::init()
        .ror(VALID_ROR_VALUES.into_iter().map(str::to_string).collect::<Vec<String>>())
        .build();
    assert!(valid.validate().is_ok());
    let invalid = ResearchActivityMetadata::init()
        .ror(vec!["https://doi.org/10.1000/182".to_string()])
        .build();
    assert!(invalid.validate().is_err());
}
#[test]
fn test_scalar_enum_validation_helpers() {
    assert!(IntegerOrString::Number(42).validate().is_ok());
    assert!(IntegerOrString::Text("42".to_string()).validate().is_ok());
    assert!(IntegerOrString::Text("  ".to_string()).validate().is_err());
    assert!(NumberOrString::Number(42.5).validate().is_ok());
    assert!(NumberOrString::Text("42.5".to_string()).validate().is_ok());
    assert!(NumberOrString::Number(f64::NAN).validate().is_err());
    assert!(NumberOrString::Text(String::new()).validate().is_err());
    assert!(YearValue::Number(2025).validate().is_ok());
    assert!(YearValue::Text("2025".to_string()).validate().is_ok());
    [
        MonthValue::Number(1),
        MonthValue::Number(12),
        MonthValue::Text("1".to_string()),
        MonthValue::Text("12".to_string()),
    ]
    .iter()
    .for_each(|value| assert!(value.validate().is_ok()));
    [MonthValue::Number(0), MonthValue::Text("13".to_string())]
        .iter()
        .for_each(|value| assert!(value.validate().is_err()));
}
#[test]
fn test_unix_epoch_rule() {
    assert!(rules::unix_epoch(1_759_017_645).is_ok());
    [42, 123_456_789].into_iter().for_each(|value| assert!(rules::unix_epoch(value).is_err()));
}
#[test]
fn test_validate_postal_code() {
    [
        PostalCode::Number(37830),
        PostalCode::Text("SW1A 1AA".to_string()),
        PostalCode::Text("75008".to_string()),
    ]
    .iter()
    .for_each(|value| assert!(value.validate().is_ok()));
    [
        PostalCode::Number(-1),
        PostalCode::Text("   ".to_string()),
        PostalCode::Text("123456789012345678901".to_string()),
        PostalCode::Text("12_345".to_string()),
    ]
    .iter()
    .for_each(|value| assert!(value.validate().is_err()));
}
#[test]
fn test_validates_lowercase_sha256_digests() {
    assert!(rules::digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").is_ok());
    assert!(rules::digest("sha256:ABCDEF").is_err());
    assert!(rules::digest("sha512:0123456789abcdef").is_err());
}