Functional Weave
Code in Rust

validation.gtin@1.1.0

impl/rust.rs

2,514 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_gs1_check_digit::is_gs1_check_digit_valid;  ← from validation.gs1-check-digit ^1.0.0 · built alongside by fune

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

/// Check an EAN-8, UPC-A, EAN-13 or GTIN-14 against the GS1 mod-10 check digit.
///
/// A pass means the number was typed correctly, not that GS1 issued it.
/// Validators answer rather than panic: a malformed value is the answer "no".
pub fn validate_gtin(value: &str) -> GtinCheck {
    // Barcodes are printed in groups, so spaces and hyphens are ignored anywhere.
    let chars: Vec<char> = value.chars().filter(|ch| *ch != ' ' && *ch != '-').collect();
    if chars.is_empty() {
        return invalid("empty");
    }
    if chars.iter().any(|ch| !ch.is_ascii_digit()) {
        return invalid("bad-character");
    }
    let kind = match chars.len() {
        8 => "ean-8",
        12 => "upc-a",
        13 => "ean-13",
        14 => "gtin-14",
        _ => return invalid("bad-length"),
    };
    // Zero everywhere satisfies the arithmetic and has never been allocated.
    if chars.iter().all(|ch| *ch == '0') {
        return invalid("all-zero");
    }

    // The GS1 mod-10 check, weighted from the right, which is what makes one
    // rule serve all four lengths (EAN-13's first digit gets 1, EAN-8's and
    // UPC-A's get 3).
    let digits: String = chars.iter().collect();
    if !is_gs1_check_digit_valid(&digits) {
        return invalid("bad-check-digit");
    }

    GtinCheck {
        valid: true,
        gtin14: Some(format!("{:0>14}", digits)),
        normalised: Some(digits),
        kind: Some(kind.to_string()),
        reason: None,
    }
}

/// Object keys are camelCase to match the shared vectors.
pub fn gtin_check_to_value(result: &GtinCheck) -> 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)),
        ("kind", text(&result.kind)),
        ("gtin14", text(&result.gtin14)),
        ("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.
    gtin_check_to_value(&validate_gtin(args[0].as_str()))
}