Functional Weave
Code in Rust

net.device-validate

Validate one network inventory entry: name, host, ports, optional MAC and subnet, with every problem listed.

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

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

What it does

Checks one entry of a network inventory file (the list of devices a monitor probes) and reports every problem with it at once, instead of the first one. An entry looks like this:

{ "name": "core-switch", "host": "10.0.0.2", "ports": [22, 443], "mac": "AA-BB-CC-DD-EE-FF", "subnet": "10.0.0.0/24" }

For example

  • validate_device(name router, host 192.168.1.1, ports 22, 80) → valid true, errors , name router, host 192.168.1.1, host kind ipv4, ports 22, 80, mac —, subnet — a minimal IPv4 entry is valid and passed through
  • validate_device(name core switch , host 10.0.0.2, ports 22, mac AA-BB-CC-DD-EE-FF, subnet 10.0.0.77/24) → valid true, errors , name core switch, host 10.0.0.2, host kind ipv4, ports 22, mac aa:bb:cc:dd:ee:ff, subnet 10.0.0.0/24 name is trimmed, MAC made canonical, subnet host bits cleared
  • validate_device(name v6, host 2001:DB8:0:0:0:0:0:1, ports 443, subnet 2001:db8::ffff/32) → valid true, errors , name v6, host 2001:db8::1, host kind ipv6, ports 443, mac —, subnet 2001:db8::/32 an IPv6 host and subnet are written in RFC 5952 form

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_device(entry: &Value) -> DeviceValidation
entryrecord{name, host, ports, mac?, subnet?} as read from an inventory file
returnsDeviceValidation

The type it declares, generated into your project

/// An inventory entry checked field by field, normalised where valid.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceValidation {
    pub valid: bool,
    /// every problem, in field order; empty when valid
    pub errors: Vec<String>,
    pub name: Option<String>,
    /// IPv4 as given, IPv6 canonical, host name lower-case
    pub host: Option<String>,
    /// ipv4, ipv6 or hostname
    pub host_kind: Option<String>,
    /// the valid ports, in order
    pub ports: Vec<i64>,
    /// canonical
    pub mac: Option<String>,
    /// canonical block
    pub subnet: Option<String>,
}

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

fune!(net.device-validate@^1);  // then call validate_device(…)
impl/rust.rs · 190 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::net_cidr::{cidr_info, is_cidr};  ← from net.cidr ^1.0.0 · built alongside by fune
use super::net_hostname_validate::validate_hostname;  ← from net.hostname-validate ^1.0.0 · built alongside by fune
use super::net_ipv4::is_ipv4;  ← from net.ipv4 ^1.0.0 · built alongside by fune
use super::net_ipv6::{format_ipv6, is_ipv6, parse_ipv6};  ← from net.ipv6 ^1.0.0 · built alongside by fune
use super::net_mac_address::validate_mac_address;  ← from net.mac-address ^1.0.0 · built alongside by fune
use super::net_port::is_port;  ← from net.port ^1.0.0 · built alongside by fune

const KNOWN: [&str; 5] = ["name", "host", "ports", "mac", "subnet"];

/// A whole number, whichever way the JSON reader stored it (22 and 22.0 are the same port).
fn whole_number(value: &Value) -> Option<i64> {
    match value {
        Value::Int(i) => Some(*i),
        Value::Float(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 9.0e15 => Some(*f as i64),
        _ => None,
    }
}

/// An IPv6 block with its host bits cleared and its address in RFC 5952 form.
fn canonical_ipv6_block(text: &str) -> String {
    let (address, prefix_text) = text.split_once('/').unwrap_or((text, "128"));
    let prefix: i64 = prefix_text.parse().unwrap_or(128);
    let groups: Vec<i64> = parse_ipv6(address)
        .iter()
        .enumerate()
        .map(|(i, g)| {
            let bits = (prefix - 16 * i as i64).clamp(0, 16);
            if bits == 0 {
                0
            } else {
                g & ((0xffff_i64 << (16 - bits)) & 0xffff)
            }
        })
        .collect();
    format!("{}/{}", format_ipv6(&groups), prefix)
}

/// Check one inventory entry and say everything that is wrong with it at once,
/// so a person fixing a file does not play whack-a-mole one error per run.
/// Never panics: a bad entry is a result, not a crash.
pub fn validate_device(entry: &Value) -> DeviceValidation {
    let mut errors: Vec<String> = Vec::new();
    let empty: Vec<(String, Value)> = Vec::new();
    let pairs = match entry {
        Value::Obj(pairs) => pairs,
        _ => &empty,
    };
    let field = |key: &str| -> Option<&Value> {
        pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v).filter(|v| !v.is_null())
    };

    let mut name: Option<String> = None;
    match field("name") {
        None => errors.push("name is required".to_string()),
        Some(Value::Str(s)) => {
            let trimmed = s.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0c' | '\x0b'));
            if trimmed.is_empty() {
                errors.push("name must not be empty".to_string());
            } else {
                name = Some(trimmed.to_string());
            }
        }
        Some(_) => errors.push("name must be text".to_string()),
    }

    let mut host: Option<String> = None;
    let mut host_kind: Option<String> = None;
    match field("host") {
        None => errors.push("host is required".to_string()),
        Some(Value::Str(s)) => {
            if is_ipv4(s) {
                host = Some(s.clone());
                host_kind = Some("ipv4".to_string());
            } else if is_ipv6(s) {
                host = Some(format_ipv6(&parse_ipv6(s)));
                host_kind = Some("ipv6".to_string());
            } else {
                let check = validate_hostname(s);
                if check.valid {
                    host = check.hostname;
                    host_kind = Some("hostname".to_string());
                } else {
                    errors.push(format!(
                        "host \"{}\" is not an IPv4 address, IPv6 address or host name: {}",
                        s,
                        check.reason.unwrap_or_default()
                    ));
                }
            }
        }
        Some(_) => errors.push("host must be text".to_string()),
    }

    let mut ports: Vec<i64> = Vec::new();
    match field("ports") {
        None => errors.push("ports is required".to_string()),
        Some(Value::Arr(items)) => {
            if items.is_empty() {
                errors.push("ports must not be empty".to_string());
            }
            for (i, item) in items.iter().enumerate() {
                match whole_number(item) {
                    None => errors.push(format!("ports[{}] must be a whole number", i)),
                    Some(port) if !is_port(port) => {
                        errors.push(format!("ports[{}] {} is not a port (1-65535)", i, port))
                    }
                    Some(port) if ports.contains(&port) => {
                        errors.push(format!("ports[{}] {} is listed twice", i, port))
                    }
                    Some(port) => ports.push(port),
                }
            }
        }
        Some(_) => errors.push("ports must be a list".to_string()),
    }

    let mut mac: Option<String> = None;
    match field("mac") {
        None => {}
        Some(Value::Str(s)) => {
            let check = validate_mac_address(s);
            if check.valid {
                mac = check.canonical;
            } else {
                errors.push(format!("mac \"{}\" is not valid: {}", s, check.reason.unwrap_or_default()));
            }
        }
        Some(_) => errors.push("mac must be text".to_string()),
    }

    let mut subnet: Option<String> = None;
    match field("subnet") {
        None => {}
        Some(Value::Str(s)) => {
            if !is_cidr(s) {
                errors.push(format!("subnet \"{}\" is not a CIDR block", s));
            } else if s.contains(':') {
                subnet = Some(canonical_ipv6_block(s));
            } else {
                subnet = Some(cidr_info(s).cidr);
            }
        }
        Some(_) => errors.push("subnet must be text".to_string()),
    }

    // Sorted, not in file order: JavaScript lists integer-like keys first, so
    // "file order" would differ between languages.
    let mut unknown: Vec<&String> = pairs.iter().map(|(k, _)| k).filter(|k| !KNOWN.contains(&k.as_str())).collect();
    unknown.sort();
    unknown.dedup();
    for key in unknown {
        errors.push(format!("unknown field \"{}\"", key));
    }

    DeviceValidation {
        valid: errors.is_empty(),
        errors,
        name,
        host,
        host_kind,
        ports,
        mac,
        subnet,
    }
}

fn opt(value: &Option<String>) -> Value {
    match value {
        Some(s) => Value::str(s),
        None => Value::Null,
    }
}

pub fn device_validation_to_value(v: &DeviceValidation) -> Value {
    Value::obj(vec![
        ("valid", Value::Bool(v.valid)),
        ("errors", Value::Arr(v.errors.iter().map(|e| Value::str(e)).collect())),
        ("name", opt(&v.name)),
        ("host", opt(&v.host)),
        ("hostKind", opt(&v.host_kind)),
        ("ports", Value::Arr(v.ports.iter().map(|p| Value::Int(*p)).collect())),
        ("mac", opt(&v.mac)),
        ("subnet", opt(&v.subnet)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    device_validation_to_value(&validate_device(&args[0]))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 6 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 net.device-validate
Download for Rust net.device-validate-1.0.0-rust.fune · 19,164 bytes sha256 55cf5c4efe7e99f445d2c67fecb94d8a15cdecc1ad3a2bcb29f849d3dc328727

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

The whole function, every language, is one file too: net.device-validate-1.0.0.fune, 29,150 bytes, sha256 f74a06dc52d0c5f4093e13b770f7f2c0508d155be2bfb2ef9ebed0ff496ad420. 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 net.device-validate

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

// fune: after net.device-validate

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 net.cidr in net.device-validate
// fune: replace net.hostname-validate in net.device-validate
// fune: replace net.ipv4 in net.device-validate
// fune: replace net.ipv6 in net.device-validate
// fune: replace net.mac-address in net.device-validate
// fune: replace net.port in net.device-validate

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 net.device-validate --steps.

// fune: step net.device-validate 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 minimal IPv4 entry is valid and passed through name router, host 192.168.1.1, ports 22, 80 → valid true, errors , name router, host 192.168.1.1, host kind ipv4, ports 22, 80, mac —, subnet —
name is trimmed, MAC made canonical, subnet host bits cleared name core switch , host 10.0.0.2, ports 22, mac AA-BB-CC-DD-EE-FF, subnet 10.0.0.77/24 → valid true, errors , name core switch, host 10.0.0.2, host kind ipv4, ports 22, mac aa:bb:cc:dd:ee:ff, subnet 10.0.0.0/24
an IPv6 host and subnet are written in RFC 5952 form name v6, host 2001:DB8:0:0:0:0:0:1, ports 443, subnet 2001:db8::ffff/32 → valid true, errors , name v6, host 2001:db8::1, host kind ipv6, ports 443, mac —, subnet 2001:db8::/32
a host name is lower-cased and loses its trailing dot name nas, host NAS.Example.COM., ports 445, mac aabb.ccdd.eeff → valid true, errors , name nas, host nas.example.com, host kind hostname, ports 445, mac aa:bb:cc:dd:ee:ff, subnet —
null optional fields are the same as leaving them out name x, host x, ports 1, mac —, subnet — → valid true, errors , name x, host x, host kind hostname, ports 1, mac —, subnet —
an empty entry lists every required field → valid false, errors name is required, host is required, ports is required, name —, host —, host kind —, ports , mac —, subnet —
wrong types are named field by field name 5, host x, ports 22, mac 1, subnet true → valid false, errors name must be text, host must be text, ports must be a list, mac must be text, subnet must be text, name —, host —, host kind —, ports , mac —, subnet —
every bad port is reported with its index; 8080.0 is port 8080 name a, host a.example, ports 0, 22.5, 80, 443, 443, 65,536, 8,080, true → valid false, errors ports[0] 0 is not a port (1-65535), ports[1] must be a whole number, ports[2] must be a whole number, ports[4] 443 is listed twice, ports[5] 65536 is not a por…
an empty port list is refused name a, host 10.0.0.1, ports → valid false, errors ports must not be empty, name a, host 10.0.0.1, host kind ipv4, ports , mac —, subnet —
192.168.1.256 is neither an address nor a host name (numeric top-level label) name bad, host 192.168.1.256, ports 80 → valid false, errors host "192.168.1.256" is not an IPv4 address, IPv6 address or host name: top-level label is all numeric, name bad, host —, host kind —, ports 80, mac —, subnet —
Show the other 7 tests
CaseArgumentsExpected
a leading-zero quad is refused rather than read as octal or as a name name octal, host 010.0.0.1, ports 80 → valid false, errors host "010.0.0.1" is not an IPv4 address, IPv6 address or host name: top-level label is all numeric, name octal, host —, host kind —, ports 80, mac —, subnet —
a doubled dot is an empty label name p, host printer..office, ports 631 → valid false, errors host "printer..office" is not an IPv4 address, IPv6 address or host name: empty label, name p, host —, host kind —, ports 631, mac —, subnet —
a blank name and an empty host name , host , ports 22 → valid false, errors name must not be empty, host "" is not an IPv4 address, IPv6 address or host name: empty, name —, host —, host kind —, ports 22, mac —, subnet —
a trailing newline makes the host invalid name nl, host 10.0.0.1 , ports 22 → valid false, errors host "10.0.0.1 " is not an IPv4 address, IPv6 address or host name: character other than a letter, digit, hyphen or dot, name nl, host —, host kind —, ports 22…
a bad MAC and a bad subnet are both reported name m, host 10.1.1.1, ports 22, mac aa:bb:cc:dd:ee:gg, subnet 10.0.0.0/33 → valid false, errors mac "aa:bb:cc:dd:ee:gg" is not valid: not a hex digit, subnet "10.0.0.0/33" is not a CIDR block, name m, host 10.1.1.1, host kind ipv4, ports 22, mac —, subnet…
a subnet without a prefix is not a CIDR block name s, host 10.1.1.1, ports 22, subnet 10.0.0.0 → valid false, errors subnet "10.0.0.0" is not a CIDR block, name s, host 10.1.1.1, host kind ipv4, ports 22, mac —, subnet —
unknown fields are reported in sorted order after everything else zone a, name x, host x, ports 1, colour b → valid false, errors unknown field "colour", unknown field "zone", name x, host x, host kind hostname, ports 1, mac —, subnet —

More from the author

`name`, `host` and `ports` are required; `mac` and `subnet` are optional (null is the same as leaving them out). The function never throws: a bad entry is a result with `valid: false` and a list of `errors`, so a monitor can report it and carry on with the rest of the file.

## What each field must be

| field | rule | built on | |-------|------|----------| | `name` | text, not blank; ASCII whitespace around it is trimmed | | | `host` | an IPv4 address, an IPv6 address or an RFC 1123 host name, tried in that order | `net.ipv4`, `net.ipv6`, `net.hostname-validate` | | `ports` | a non-empty list of whole numbers 1-65535, each once | `net.port` | | `mac` | an EUI-48 MAC in any of the forms `net.mac-address` accepts | `net.mac-address` | | `subnet` | an IPv4 or IPv6 CIDR block | `net.cidr` |

Anything else in the entry is reported as `unknown field "x"`, which catches typos such as `"port"` for `"ports"`. Unknown fields are listed in sorted order after the field errors, because JavaScript orders integer-like keys first and "file order" would differ between languages.

## The normal forms returned

- `host`: an IPv4 address as given (it can only be valid in one spelling), IPv6 in RFC 5952 canonical form, a host name lower-cased without its trailing dot. `hostKind` says which of `ipv4`, `ipv6` or `hostname` it is. - `ports`: the valid ports in order, a repeated one only once. `22.0` is port 22, since a JSON reader may store it as a float. - `mac`: lower-case, colon-separated. - `subnet`: host bits cleared (`10.0.0.77/24` is `10.0.0.0/24`), the address of an IPv6 block in RFC 5952 form.

Fields that fail are null (or left out of `ports`) in the result, while the rest are still normalised, so a report can name the device whose MAC is bad.

## Edge cases

- `192.168.1.256` and `010.0.0.1` are not IPv4 addresses (out of range, and a leading zero that some parsers read as octal), and they are not host names either: a top-level label may not be all digits (RFC 3696 section 2). A naive check lets them through as names. - A trailing newline is not trimmed from `host`; it makes the entry invalid. - Subnet membership is not checked here: a host name has no address until it is resolved, which is I/O. Check it afterwards with `net.cidr`'s `cidrContains` on the resolved address.

Files

PathBytes
README.md2,654
impl/python.py4,887
impl/rust.rs6,750
impl/typescript.ts4,582
vectors.json5,958