Functional Weave
Code in TypeScript

validation.lei@1.0.0

impl/rust.rs

2,629 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

fn invalid(reason: &str) -> LeiCheck {
    LeiCheck {
        valid: false,
        normalised: None,
        lou_prefix: None,
        reason: Some(reason.to_string()),
    }
}

/// ISO 7064 MOD 97-10 over the whole LEI, letters read as A = 10 ... Z = 35.
///
/// The expansion is up to 40 digits, past any 64-bit integer, so the remainder
/// is carried one character at a time. A letter contributes two digits and so
/// multiplies the running remainder by 100; the largest intermediate is
/// 96 * 100 + 35.
fn mod97(lei: &[char]) -> u32 {
    let mut remainder: u32 = 0;
    for ch in lei {
        if ch.is_ascii_digit() {
            remainder = (remainder * 10 + (*ch as u32 - 48)) % 97;
        } else {
            remainder = (remainder * 100 + (*ch as u32 - 55)) % 97;
        }
    }
    remainder
}

/// Check a Legal Entity Identifier's format and ISO 17442 check digits.
///
/// Well formed is not issued: only GLEIF says whether an LEI is current.
/// Validators answer rather than panic.
pub fn validate_lei(value: &str) -> LeiCheck {
    let lei: Vec<char> = value
        .chars()
        .filter(|ch| *ch != ' ')
        .map(|ch| ch.to_ascii_uppercase())
        .collect();
    if lei.is_empty() {
        return invalid("empty");
    }
    if lei.iter().any(|ch| !(ch.is_ascii_digit() || ch.is_ascii_uppercase())) {
        return invalid("bad-character");
    }
    if lei.len() != 20 {
        return invalid("bad-length");
    }
    if !lei[18].is_ascii_digit() || !lei[19].is_ascii_digit() {
        return invalid("bad-format");
    }
    // The remainder must be 1, not 0: the check digits are chosen so that it is.
    if mod97(&lei) != 1 {
        return invalid("bad-check-digit");
    }

    LeiCheck {
        valid: true,
        normalised: Some(lei.iter().collect()),
        lou_prefix: Some(lei[..4].iter().collect()),
        reason: None,
    }
}

/// Object keys are camelCase to match the shared vectors.
pub fn lei_check_to_value(result: &LeiCheck) -> Value {
    let text = |field: &Option<String>| match field {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    Value::obj(vec![
        ("valid", Value::Bool(result.valid)),
        ("normalised", text(&result.normalised)),
        ("louPrefix", text(&result.lou_prefix)),
        ("reason", text(&result.reason)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A non-string argument arrives here as an empty string, which is exactly
    // the answer TypeScript and Python give for a non-string: empty.
    lei_check_to_value(&validate_lei(args[0].as_str()))
}