Functional Weave
Code in Rust

validation.uk-company-number@2.0.1

impl/rust.rs

3,800 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
use super::validation_uk_company_number_data::COMPANY_PREFIXES;  ← this capability’s own data, compiled from data/company-prefixes.json into the same file by fune build

/// Every company number is eight characters: eight digits, or a two-character
/// prefix and six digits.
const LENGTH: usize = 8;

fn invalid(reason: &str) -> UkCompanyNumber {
    UkCompanyNumber {
        valid: false,
        normalised: None,
        prefix: None,
        jurisdiction: None,
        company_type: None,
        reason: Some(reason.to_string()),
    }
}

/// Check a Companies House company number's shape and prefix.
///
/// There is no check digit, so a pass means the number is shaped like one
/// Companies House issues under a prefix it uses, not that the company exists:
/// that is a lookup on the register. Checks run in a fixed order (characters,
/// shape, length, prefix) so every language gives the same first reason.
pub fn validate_uk_company_number(value: &str) -> UkCompanyNumber {
    let mut compact = String::new();
    for ch in value.chars() {
        if ch == ' ' {
            continue;
        }
        // ASCII-only upper-casing, so every language folds identically.
        let ch = ch.to_ascii_uppercase();
        if !ch.is_ascii_uppercase() && !ch.is_ascii_digit() {
            return invalid("bad-character");
        }
        compact.push(ch);
    }
    if compact.is_empty() {
        return invalid("empty");
    }

    // Everything is ASCII by now, so byte indexing is character indexing.
    let bytes = compact.as_bytes();
    // R0 (pre-partition Northern Ireland) is the one prefix containing a digit.
    let prefix_len = if compact.starts_with("R0")
        || (bytes.len() >= 2 && bytes[0].is_ascii_uppercase() && bytes[1].is_ascii_uppercase())
    {
        2
    } else if bytes[0].is_ascii_uppercase() {
        return invalid("bad-format");
    } else {
        0
    };
    let prefix = &compact[..prefix_len];
    let digits = &compact[prefix_len..];
    if !digits.bytes().all(|b| b.is_ascii_digit()) {
        return invalid("bad-format");
    }
    let width = LENGTH - prefix_len;
    if digits.is_empty() || digits.len() > width {
        return invalid("bad-length");
    }
    // Leading zeros are part of the number and are often dropped when typed,
    // so they are put back; a number of nothing but zeros is no company.
    if digits.bytes().all(|b| b == b'0') {
        return invalid("bad-format");
    }

    match COMPANY_PREFIXES.iter().find(|row| row.prefix == prefix) {
        Some(row) => UkCompanyNumber {
            valid: true,
            normalised: Some(format!("{}{}{}", prefix, "0".repeat(width - digits.len()), digits)),
            prefix: Some(prefix.to_string()),
            jurisdiction: Some(row.jurisdiction.to_string()),
            company_type: Some(row.company_type.to_string()),
            reason: None,
        },
        None => invalid("unknown-prefix"),
    }
}

/// Object keys are camelCase to match the shared vectors, and so that a
/// capability building on this one can reuse the same shape.
pub fn uk_company_number_to_value(result: &UkCompanyNumber) -> 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)),
        ("prefix", text(&result.prefix)),
        ("jurisdiction", text(&result.jurisdiction)),
        ("companyType", text(&result.company_type)),
        ("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.
    uk_company_number_to_value(&validate_uk_company_number(args[0].as_str()))
}