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()))
}