Functional Weave
Code in Rust

auth.validate-registration@1.0.0

impl/rust.rs

3,208 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::auth_normalise_email::normalise_email;  ← from auth.normalise-email ^1.0.0 · built alongside by fune
use super::auth_password_policy_check_password::{check_password, password_policy_from_value};
use super::auth_password_policy_types::PasswordPolicy;

const MAX_NAME: usize = 100;

fn is_ascii_space(ch: char) -> bool {
    matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{0C}' | '\u{0B}')
}

/// A sign-up form's email, password and name checked together: the values
/// to store when valid, and a message for each field that needs fixing.
///
/// # Panics
/// Panics on a policy whose numbers make no sense.
pub fn validate_registration(email: &str, password: &str, name: &str, policy: &PasswordPolicy) -> RegistrationCheck {
    let mut fields: Vec<(String, String)> = Vec::new();

    let normalised = normalise_email(email);
    if normalised.is_none() {
        let message = if email.trim_matches(is_ascii_space).is_empty() {
            "Enter your email address."
        } else {
            "Enter a valid email address, like name@example.com."
        };
        fields.push(("email".to_string(), message.to_string()));
    }

    let trimmed_name = name.trim_matches(is_ascii_space);
    let count = trimmed_name.chars().count();
    let name_error: Option<String> = if count == 0 {
        Some("Enter your name.".to_string())
    } else if count > MAX_NAME {
        Some(format!("Use no more than {} characters for your name.", MAX_NAME))
    } else if trimmed_name.chars().any(|ch| (ch as u32) < 0x20 || ch as u32 == 0x7F) {
        Some("Your name cannot contain control characters.".to_string())
    } else {
        None
    };

    let check = check_password(
        password,
        normalised.as_deref(),
        if name_error.is_none() { Some(trimmed_name) } else { None },
        policy,
    );
    if password.is_empty() {
        fields.push(("password".to_string(), "Enter a password.".to_string()));
    } else if !check.valid {
        let joined: Vec<&str> = check.failures.iter().map(|f| f.message.as_str()).collect();
        fields.push(("password".to_string(), joined.join(" ")));
    }
    let valid_name = name_error.is_none();
    if let Some(message) = name_error {
        fields.push(("name".to_string(), message));
    }

    RegistrationCheck {
        valid: fields.is_empty(),
        email: normalised,
        name: if valid_name { Some(trimmed_name.to_string()) } else { None },
        fields,
    }
}

pub fn registration_check_to_value(check: &RegistrationCheck) -> Value {
    let text = |v: &Option<String>| v.as_deref().map(Value::str).unwrap_or(Value::Null);
    Value::obj(vec![
        ("valid", Value::Bool(check.valid)),
        ("email", text(&check.email)),
        ("name", text(&check.name)),
        (
            "fields",
            Value::Obj(check.fields.iter().map(|(k, v)| (k.clone(), Value::str(v))).collect()),
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A non-string is an empty field, as in TypeScript and Python.
    let policy = password_policy_from_value(&args[3]);
    registration_check_to_value(&validate_registration(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
        &policy,
    ))
}