Functional Weave
Code in Rust

auth.validate-registration

Validate a sign-up form (email, password, name) in one call, with a message per field, in the browser and the API alike.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript, Python and Rust.

What it does

One call checks a whole sign-up form and answers in the shape an API's `validation_failed` error and a form both want:

validateRegistration(" Ada@Example.com ", "short", "Ada Lovelace", passwordPolicy("nist-800-63b-4-single-factor"))
# {valid: false, email: "Ada@example.com", name: "Ada Lovelace",
#  fields: {"password": "Use at least 15 characters."}}

For example

  • validate_registration( Ada@Example.COM , correct horse battery staple, Ada Lovelace , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, blo…) → valid true, email Ada@example.com, name Ada Lovelace, fields … a good form: the email normalised and the name trimmed for storage
  • validate_registration(ada@example.com, short, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true) → valid false, email ada@example.com, name Ada Lovelace, fields … a short password
  • validate_registration(ada@example.com, password, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true) → valid false, email ada@example.com, name Ada Lovelace, fields … several password failures are joined into one message

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn validate_registration(email: &str, password: &str, name: &str, policy: &PasswordPolicy) -> RegistrationCheck
emailstringas typed; normalised with auth.normalise-email
passwordstringas typed; checked with auth.password-policy against the email and name
namestringas typed; trimmed, 1 to 100 characters, no control characters
policyPasswordPolicyusually passwordPolicy("nist-800-63b-4-single-factor")
returnsRegistrationCheckvalid with the values to store, or a message for each field that needs fixing

The type it declares, generated into your project

/// A sign-up form's verdict, shaped for an API's validation error and a form's field messages.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistrationCheck {
    pub valid: bool,
    /// normalised, when the email is valid
    pub email: Option<String>,
    /// trimmed, when the name is valid
    pub name: Option<String>,
    /// field name to message, only for fields that need fixing; empty when valid
    pub fields: Vec<(String, String)>,
}

Your code names it in one line, in the file that uses it

fune!(auth.validate-registration@^1);  // then call validate_registration(…)
impl/rust.rs · 89 lines · open · 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,
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add auth.validate-registration
Download for Rust auth.validate-registration-1.0.0-rust.fune · 15,285 bytes sha256 eb92507e265b38e4162609056dd2c65478adb80a82cf91b7e0a74bf4bd6309ef

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./auth.validate-registration-1.0.0-rust.fune, or fetch it from a terminal with fune pull auth.validate-registration@1.0.0:rust.

The whole function, every language, is one file too: auth.validate-registration-1.0.0.fune, 20,018 bytes, sha256 61f73a1f82c59d4baea20cec7e33b7e61e88780f6af7fc6438ddca4bad4208e4. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before auth.validate-registration

after — your function gets the result and the arguments, and returns the final result.

// fune: after auth.validate-registration

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace auth.normalise-email in auth.validate-registration
// fune: replace auth.password-policy in auth.validate-registration

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show auth.validate-registration --steps.

// fune: step auth.validate-registration after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a good form: the email normalised and the name trimmed for storage Ada@Example.COM , correct horse battery staple, Ada Lovelace , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, blo… → valid true, email Ada@example.com, name Ada Lovelace, fields …
a short password ada@example.com, short, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email ada@example.com, name Ada Lovelace, fields …
several password failures are joined into one message ada@example.com, password, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email ada@example.com, name Ada Lovelace, fields …
the password contains the (normalised) email's local part and the trimmed name Ada@example.com, ada-lovelace-rules-ok, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal … → valid false, email Ada@example.com, name Ada Lovelace, fields …
every field empty , , , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email —, name —, fields …
an email that is only spaces counts as empty , correct horse battery staple, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email —, name Ada, fields …
an email that is not an address ada@localhost, correct horse battery staple, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email —, name Ada, fields …
a name of 101 characters ada@example.com, correct horse battery staple, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, name nist-800-63b-4-single-fa… → valid false, email ada@example.com, name —, fields …
a name of exactly 100 characters, counted in code points (emoji are one each) ada@example.com, correct horse battery staple, 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀… → valid true, email ada@example.com, name 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀�…
a control character inside the name ada@example.com, correct horse battery staple, AdaLovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block pe… → valid false, email ada@example.com, name —, fields …
Show the other 4 tests
CaseArgumentsExpected
names in any script are fine li@example.cn, correct horse battery staple, 李小龍, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid true, email li@example.cn, name 李小龍, fields …
the multi-factor policy accepts 8 characters ada@example.com, tr0ub4dor, Ada Lovelace, name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true → valid true, email ada@example.com, name Ada Lovelace, fields …
a password that is not a string (a malformed JSON body) is treated as empty ada@example.com, 12,345, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true → valid false, email ada@example.com, name Ada, fields …
a nonsensical policy is the caller's error ada@example.com, x, Ada, name nist-800-63b-4-single-factor, min length 0, max length 128, min character classes 0, block common true, block personal true → error: policy minLength must be a whole number of at least 1

More from the author

The browser runs it on submit to show messages beside the fields; the API runs the very same function and returns `fields` in its 400 response, so the two can never disagree about what is acceptable. When `valid` is true, store `email` and `name` from the result (normalised and trimmed), never the raw input.

**email** goes through `auth.normalise-email` (trimmed, domain lower-cased). Empty after trimming: "Enter your email address."; otherwise not an address `validation.email` accepts: "Enter a valid email address, like name@example.com."

**password** goes through `auth.password-policy`'s `checkPassword`, with the normalised email and trimmed name as the personal words to refuse. Empty: "Enter a password."; otherwise every failure's message, joined with a space in the policy's fixed order, so the field shows the whole story ("Use at least 15 characters. This password is too common. Choose something harder to guess.").

**name** is trimmed of ASCII whitespace and must then be 1 to 100 characters (code points), with no control characters (U+0000 to U+001F and U+007F): "Enter your name.", "Use no more than 100 characters for your name." or "Your name cannot contain control characters.". Any script is welcome; names are not otherwise judged (see "Falsehoods Programmers Believe About Names").

`fields` lists only the fields that need fixing, keyed `email`, `password`, `name`, in that order. A value that is not a string (a JSON body with a number where the password goes) is treated as empty, so a malformed request gets field messages rather than an exception. A nonsensical policy throws, as in `checkPassword`.

Whether the email is already taken is not a rule a pure function can know; the API checks its database after this passes (409 `email_taken`).

Files

PathBytes
README.md2,172
impl/python.py2,060
impl/rust.rs3,208
impl/typescript.ts2,435
vectors.json6,412