# helpers4 (Rust) — full reference, version 0.0.5 > Every public item of the `helpers4` crate with its signature, documentation and examples. > The examples are doctests that run in the crate's CI: treat them as verified usage. > Import through the module path; names repeat across modules on purpose. > Each module is a Cargo feature of the same name (`cargo add helpers4 --no-default-features --features `). > Pre-1.0 (0.x): the split into modules (Cargo features) may change between releases, and the verification tooling is still being extended. ## Module `array` (Cargo feature `array`) Helpers for slices and `Vec`s that the standard library does not provide. Inputs are borrowed slices and results are new `Vec`s: nothing is mutated. Membership-based helpers require `Eq + Hash`. ### array::cartesian_product ```rust pub fn cartesian_product(a: &[A], b: &[B]) -> Vec<(A, B)> ``` Returns every pair `(x, y)` with `x` from `a` and `y` from `b`, in row-major order. For more than two inputs, nest the calls. #### Arguments - `a` - The first slice. - `b` - The second slice. #### Examples ```rust use helpers4::array::cartesian_product; assert_eq!( cartesian_product(&[1, 2], &['a', 'b']), vec![(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')] ); ``` ### array::count_by ```rust pub fn count_by(items: &[T], mut key: impl FnMut(&T) -> K) -> HashMap ``` Counts the elements of `items` per key returned by `key`. #### Arguments - `items` - The elements to count. - `key` - Returns the key to count each element under. #### Examples ```rust use helpers4::array::count_by; let counts = count_by(&[1, 2, 3, 4, 5], |n| if n % 2 == 0 { "even" } else { "odd" }); assert_eq!(counts["odd"], 3); assert_eq!(counts["even"], 2); ``` ### array::difference ```rust pub fn difference(a: &[T], b: &[T]) -> Vec ``` Returns the elements of `a` that are not in `b`, in `a`'s order. Duplicates in `a` are kept: only membership in `b` decides whether an element stays. #### Arguments - `a` - The elements to keep from. - `b` - The elements to remove. #### Examples ```rust use helpers4::array::difference; assert_eq!(difference(&[1, 2, 3, 2], &[2]), vec![1, 3]); assert_eq!(difference(&[1, 1, 2], &[3]), vec![1, 1, 2]); ``` ### array::duplicates ```rust pub fn duplicates(items: &[T]) -> Vec ``` Returns the elements that appear more than once in `items`, each one once, in the order they first appear. The counterpart of `unique`: what `unique` keeps, this reports as repeated. #### Arguments - `items` - The elements to scan. #### Examples ```rust use helpers4::array::duplicates; assert_eq!(duplicates(&[1, 2, 3, 2, 1, 2]), vec![1, 2]); assert_eq!(duplicates(&["a", "b", "c"]), Vec::<&str>::new()); ``` ### array::equals_unordered ```rust pub fn equals_unordered(a: &[T], b: &[T]) -> bool ``` Returns `true` when `a` and `b` hold the same elements the same number of times, in any order. Use it for collections where order is meaningless (tags, ids). For positional equality, compare the slices with `==`. #### Arguments - `a` - The first slice. - `b` - The second slice. #### Examples ```rust use helpers4::array::equals_unordered; assert!(equals_unordered(&[1, 2, 2, 3], &[3, 2, 1, 2])); assert!(!equals_unordered(&[1, 2, 2], &[1, 1, 2])); ``` ### array::group_by ```rust pub fn group_by( items: &[T], mut key: impl FnMut(&T) -> K, ) -> HashMap> ``` Groups the elements of `items` by the key returned by `key`. Within each group, elements keep their original order. The order of the groups themselves is unspecified (`HashMap`). #### Arguments - `items` - The elements to group. - `key` - Returns the key to group each element under. #### Examples ```rust use helpers4::array::group_by; let groups = group_by(&[1, 2, 3, 4, 5], |n| n % 2 == 0); assert_eq!(groups[&true], vec![2, 4]); assert_eq!(groups[&false], vec![1, 3, 5]); ``` ### array::interleave ```rust pub fn interleave(a: &[T], b: &[T]) -> Vec ``` Alternates the elements of `a` and `b`, starting with `a`; the leftover of the longer slice goes at the end. #### Arguments - `a` - The slice to take the first, third, … elements from. - `b` - The slice to take the second, fourth, … elements from. #### Examples ```rust use helpers4::array::interleave; assert_eq!(interleave(&[1, 3, 5], &[2, 4, 6]), vec![1, 2, 3, 4, 5, 6]); assert_eq!(interleave(&["a", "b", "c"], &["x"]), vec!["a", "x", "b", "c"]); ``` ### array::intersection ```rust pub fn intersection(a: &[T], b: &[T]) -> Vec ``` Returns the elements of `a` that also appear in `b`, in `a`'s order. Duplicates in `a` are kept: only membership in `b` decides whether an element stays. #### Arguments - `a` - The first slice. - `b` - The second slice. #### Examples ```rust use helpers4::array::intersection; assert_eq!(intersection(&[1, 2, 3, 2], &[2, 3, 4]), vec![2, 3, 2]); assert_eq!(intersection(&[1], &[2]), Vec::::new()); ``` ### array::intersects ```rust pub fn intersects(a: &[T], b: &[T]) -> bool ``` Returns `true` when `a` and `b` share at least one element. #### Arguments - `a` - The first slice. - `b` - The second slice. #### Examples ```rust use helpers4::array::intersects; assert!(intersects(&[1, 2, 3], &[3, 4])); assert!(!intersects(&[1, 2], &[3, 4])); ``` ### array::key_by ```rust pub fn key_by(items: &[T], mut key: impl FnMut(&T) -> K) -> HashMap ``` Indexes the elements of `items` by the key returned by `key`. When several elements share a key, the last one wins. Use `group_by` to keep them all. #### Arguments - `items` - The elements to index. - `key` - Returns the key to index each element under. #### Examples ```rust use helpers4::array::key_by; let users = [("ann", 1), ("bob", 2), ("cat", 1)]; let by_id = key_by(&users, |&(_, id)| id); assert_eq!(by_id[&2], ("bob", 2)); assert_eq!(by_id[&1], ("cat", 1)); ``` ### array::symmetric_difference ```rust pub fn symmetric_difference(a: &[T], b: &[T]) -> Vec ``` Returns the elements present in exactly one of `a` and `b`. The result is the elements of `a` missing from `b` (in `a`'s order), followed by the elements of `b` missing from `a` (in `b`'s order). Duplicates are kept. #### Arguments - `a` - The first slice. - `b` - The second slice. #### Examples ```rust use helpers4::array::symmetric_difference; assert_eq!(symmetric_difference(&[1, 2, 3], &[2, 3, 4]), vec![1, 4]); ``` ### array::unique ```rust pub fn unique(items: &[T]) -> Vec ``` Removes duplicate values, keeping the first occurrence of each and the original order. #### Arguments - `items` - The elements to deduplicate. #### Examples ```rust use helpers4::array::unique; assert_eq!(unique(&[1, 2, 1, 3, 2]), vec![1, 2, 3]); assert_eq!(unique::(&[]), Vec::::new()); ``` ### array::unique_by ```rust pub fn unique_by(items: &[T], mut key: impl FnMut(&T) -> K) -> Vec ``` Removes elements whose `key` was already seen, keeping the first of each key in order. #### Arguments - `items` - The elements to deduplicate. - `key` - Returns the key that decides which elements are duplicates. #### Examples ```rust use helpers4::array::unique_by; let words = ["apple", "avocado", "banana", "blueberry", "cherry"]; assert_eq!( unique_by(&words, |w| w.chars().next()), vec!["apple", "banana", "cherry"] ); ``` ## Module `cache` (Cargo feature `cache`) Caches and stores whose entries expire. The clock is always passed in, never read. ### cache::ExpiringMap ```rust pub struct ExpiringMap { /* private fields */ } ``` A map whose entries expire, with the clock passed in by the caller. Nothing here reads a clock, so it is trivially testable and works with any timestamp: pass unix seconds (`u64`, e.g. the `exp` of a token), milliseconds, or an `Instant`. An entry is live while `now < expires_at`. Expired entries are dropped lazily: every write sweeps them, so the map only grows with the entries inserted inside one lifetime window (a replay-protection store keyed by token id, a cache of pending challenges, ...). The sweep is skipped in constant time while nothing can have expired yet, and is a single pass otherwise. `len` counts entries still stored, expired or not, until the next write sweeps them: call `evict_expired` first when comparing it to a threshold. There is **no capacity bound**. If the keys come from untrusted input and their lifetimes are long, the number of live entries is only limited by the insert rate times the lifetime: bound it yourself (reject or rate-limit inserts) when that matters. #### Examples ```rust use helpers4::cache::ExpiringMap; let mut seen: ExpiringMap<&str, (), u64> = ExpiringMap::new(); let now = 1_000; // First use of a token id, valid until t = 1_060: accepted. assert!(seen.insert_if_absent("jti-1", (), 1_060, now)); // The same id again while it is live: a replay. assert!(!seen.insert_if_absent("jti-1", (), 1_060, now + 10)); // Once it has expired it can be used (and remembered) again. assert!(seen.insert_if_absent("jti-1", (), 1_200, 1_060)); ``` #### ExpiringMap::new ```rust pub fn new() -> Self ``` Creates an empty map. ##### Returns A new, empty `ExpiringMap`. #### ExpiringMap::len ```rust pub fn len(&self) -> usize ``` The number of stored entries, including expired ones that no write has swept yet. ##### Returns The number of entries currently stored, including any that have expired but were not evicted yet. #### ExpiringMap::is_empty ```rust pub fn is_empty(&self) -> bool ``` Whether nothing is stored (see `len`). ##### Returns `true` when the map stores no entries at all, including expired ones not yet evicted. #### ExpiringMap::clear ```rust pub fn clear(&mut self) ``` Removes every entry, expired or not. #### ExpiringMap::insert ```rust pub fn insert(&mut self, key: K, value: V, expires_at: T, now: T) -> Option ``` Stores `value` under `key` until `expires_at`, replacing any live entry, and returns the replaced value. Expired entries are swept first. An `expires_at` that is not after `now` expires immediately. ##### Arguments - `key` - The key to store the value under. - `value` - The value to store. - `expires_at` - The point in time at which the entry becomes invisible. - `now` - The current time, used to evict already-expired entries before inserting. ##### Returns The previous value for `key`, if there was one and it had not expired yet. #### ExpiringMap::insert_if_absent ```rust pub fn insert_if_absent(&mut self, key: K, value: V, expires_at: T, now: T) -> bool ``` Stores `value` under `key` only if there is no live entry for it, and returns whether it did. This is the replay check: `false` means the key was already seen and is still live. Expired entries are swept first. ##### Arguments - `key` - The key to store the value under. - `value` - The value to store. - `expires_at` - The point in time at which the entry becomes invisible. - `now` - The current time, used to decide whether an existing entry has already expired. ##### Returns `true` when the value was inserted, `false` when `key` already had a live entry. #### ExpiringMap::get ```rust pub fn get(&self, key: &K, now: T) -> Option<&V> ``` The live value under `key`, or `None` if absent or expired at `now`. ##### Arguments - `key` - The key to look up. - `now` - The current time, used to decide whether the entry has expired. ##### Returns The value for `key`, or `None` when it is missing or expired. #### ExpiringMap::contains_key ```rust pub fn contains_key(&self, key: &K, now: T) -> bool ``` Whether there is a live entry under `key` at `now`. ##### Arguments - `key` - The key to look up. - `now` - The current time, used to decide whether the entry has expired. ##### Returns `true` when `key` has a live entry. #### ExpiringMap::remove ```rust pub fn remove(&mut self, key: &K, now: T) -> Option ``` Removes `key` and returns its value if it was still live at `now`. ##### Arguments - `key` - The key to remove. - `now` - The current time, used to decide whether the entry had already expired. ##### Returns The removed value, or `None` when there was no live entry for `key`. #### ExpiringMap::evict_expired ```rust pub fn evict_expired(&mut self, now: T) -> usize ``` Drops every entry that has expired at `now` and returns how many. Writes do this already; call it directly to release memory while nothing is being written. ##### Arguments - `now` - The current time. ##### Returns The number of entries removed. ### cache::ExpiringSet ```rust pub type ExpiringSet = ExpiringMap ``` A set whose members expire: an `ExpiringMap` without values. ## Module `duration` (Cargo feature `duration`) Parsing and formatting of durations as short human-readable strings (`1h30m`). The strings use the units `ms`, `s`, `m`, `h`, `d` and `w`. Values are `std::time::Duration`. ### duration::format ```rust pub fn format(duration: Duration) -> String ``` Formats `duration` as a short human-readable string such as `"1h 30m 5s"`. Uses the units `d`, `h`, `m`, `s` and `ms`, largest first, and skips the ones that are zero. A zero duration is `"0s"`. Anything below one millisecond is dropped (truncated, not rounded). `parse` reads the result back. #### Arguments - `duration` - The duration to format. #### Examples ```rust use helpers4::duration::format; use std::time::Duration; assert_eq!(format(Duration::from_secs(5405)), "1h 30m 5s"); assert_eq!(format(Duration::from_millis(1500)), "1s 500ms"); assert_eq!(format(Duration::ZERO), "0s"); ``` ### duration::parse ```rust pub fn parse(input: &str) -> Result ``` Parses a human-written duration such as `"1h30m"`, `"2d"` or `"500ms"`. The input is one or more `` pairs, optionally separated by whitespace, and their sum is returned. Units are `ms`, `s`, `m`, `h`, `d` (24 hours) and `w` (7 days), and may repeat or come in any order. A bare number, a fraction, a sign or an unknown unit is an error with the byte offset where it was found. It is the inverse of `format`. #### Arguments - `input` - The human-written duration, such as `"1h30m"`. #### Errors Returns a `ParseDurationError` when the input is empty, has something other than a number where one is expected, a number without a unit, an unknown unit, or a total that does not fit in a `Duration`. #### Examples ```rust use helpers4::duration::parse; use std::time::Duration; assert_eq!(parse("1h30m"), Ok(Duration::from_secs(5400))); assert_eq!(parse("2d 12h"), Ok(Duration::from_secs(216_000))); assert_eq!(parse("1500ms"), Ok(Duration::from_millis(1500))); assert!(parse("90").is_err()); ``` ### duration::ParseDurationError ```rust #[non_exhaustive] pub enum ParseDurationError { /// The string is empty or only whitespace. Empty, /// A number was expected but something else was found. ExpectedNumber { /// Byte offset of the offending character in the input. index: usize, }, /// A number is not followed by a unit. MissingUnit { /// Byte offset where the unit was expected. index: usize, }, /// The unit is not one of `ms`, `s`, `m`, `h`, `d`, `w`. UnknownUnit { /// Byte offset of the unit in the input. index: usize, }, /// The total does not fit in a [`Duration`](std::time::Duration). Overflow, } ``` Why a string could not be parsed as a duration. ## Module `env` (Cargo feature `env`) Dotenv (`.env`) helpers working on plain text: no file or process-environment access, so they are deterministic and easy to test. Read the file yourself, edit the content here, and write it back. ### env::get ```rust pub fn get(content: &str, key: &str) -> Option ``` Returns the value of `key` in dotenv `content`, or `None` when it is not assigned. When a key is assigned more than once the last assignment wins, like a shell sourcing the file. See `parse` for the accepted syntax. #### Arguments - `content` - The dotenv text to read. - `key` - The variable name to look up. #### Examples ```rust use helpers4::env::get; let content = "HOST=localhost\nPORT=80\nPORT=8080\n"; assert_eq!(get(content, "PORT").as_deref(), Some("8080")); assert_eq!(get(content, "MISSING"), None); ``` ### env::parse ```rust pub fn parse(content: &str) -> Vec<(String, String)> ``` Parses dotenv `content` into `(key, value)` pairs, in file order. Blank lines, `#` comments and lines that are not valid `KEY=value` assignments are skipped. Supported: an optional `export ` prefix, bare values (a `#` after whitespace starts a comment), `"double quoted"` values with `\n \r \t \" \\` escapes and `'single quoted'` literals. Values are single-line. A key assigned twice appears twice. #### Arguments - `content` - The dotenv text to parse. #### Examples ```rust use helpers4::env::parse; let vars = parse("# comment\nHOST=localhost\nexport NAME=\"my app\" # trailing\n"); assert_eq!( vars, vec![ ("HOST".to_string(), "localhost".to_string()), ("NAME".to_string(), "my app".to_string()), ] ); ``` ### env::remove ```rust pub fn remove(content: &str, key: &str) -> String ``` Removes every assignment of `key` from dotenv `content` and returns the new content. Comments, blank lines and other variables are left untouched. Removing a key that is not assigned returns the content unchanged. #### Arguments - `content` - The dotenv text to edit. - `key` - The variable name to remove every assignment of. #### Examples ```rust use helpers4::env::remove; assert_eq!(remove("A=1\nB=2\nA=3\n", "A"), "B=2\n"); ``` ### env::set ```rust pub fn set(content: &str, key: &str, value: &str) -> Result ``` Sets `key` to `value` in dotenv `content` and returns the new content. The first existing assignment of `key` is replaced in place and any later ones are dropped; when there is none, a new line is appended. Every other line (comments, blank lines, other variables) is left untouched, and the replaced line keeps its line ending. `value` is quoted and escaped only when needed, so `get` reads it back exactly. #### Arguments - `content` - The dotenv text to edit. - `key` - The variable name to set. - `value` - The value to assign to `key`. #### Errors Returns `InvalidKeyError` when `key` is not `[A-Za-z_][A-Za-z0-9_]*`. #### Examples ```rust use helpers4::env::set; let updated = set("# config\nHOST=old\nPORT=80\n", "HOST", "example.com")?; assert_eq!(updated, "# config\nHOST=example.com\nPORT=80\n"); assert_eq!(set("A=1\n", "B", "two words")?, "A=1\nB=\"two words\"\n"); ``` ### env::InvalidKeyError ```rust pub struct InvalidKeyError { /* private fields */ } ``` The variable name passed to `set` is not a valid name (`[A-Za-z_][A-Za-z0-9_]*`). #### InvalidKeyError::key ```rust pub fn key(&self) -> &str ``` The rejected name. ##### Returns The rejected variable name. ## Module `hex` (Cargo feature `hex`) Hexadecimal encoding and decoding with typed errors. Decoding accepts either case and rejects anything that is not pairs of hex digits, so trim whitespace and strip `0x` prefixes before calling it. ### hex::decode ```rust pub fn decode(hex: &str) -> Result, DecodeError> ``` Decodes a hexadecimal string (either case) into bytes. The string must be made of digit pairs only: surrounding whitespace, a `0x` prefix or separators are errors, so trim or strip them first. #### Arguments - `hex` - The hexadecimal string to decode. #### Errors `DecodeError::OddLength` for an odd number of characters, `DecodeError::InvalidChar` for a character that is not a hex digit. #### Examples ```rust use helpers4::hex::decode; assert_eq!(decode("DeadBeef")?, vec![0xde, 0xad, 0xbe, 0xef]); assert!(decode("abc").is_err()); ``` ### hex::decode_array ```rust pub fn decode_array(hex: &str) -> Result<[u8; N], DecodeError> ``` Decodes a hexadecimal string into a fixed-size array, e.g. a 32-byte key from 64 hex digits. #### Arguments - `hex` - The hexadecimal string to decode; must encode exactly `N` bytes. #### Errors Same as `decode_to_slice`: the string must have exactly `2 * N` hex digits. #### Examples ```rust use helpers4::hex::decode_array; let key: [u8; 4] = decode_array("deadbeef")?; assert_eq!(key, [0xde, 0xad, 0xbe, 0xef]); assert!(decode_array::<4>("dead").is_err()); ``` ### hex::decode_to_slice ```rust pub fn decode_to_slice(hex: &str, out: &mut [u8]) -> Result<(), DecodeError> ``` Decodes a hexadecimal string into `out`, which must be exactly half as long as the string. Nothing is allocated; on error `out` may be partially written. #### Arguments - `hex` - The hexadecimal string to decode. - `out` - The buffer to decode into; must be exactly half of `hex`'s length. #### Errors `DecodeError::OddLength`, `DecodeError::InvalidLength` when the string does not match `out.len() * 2`, or `DecodeError::InvalidChar`. #### Examples ```rust use helpers4::hex::decode_to_slice; let mut buf = [0u8; 2]; decode_to_slice("beef", &mut buf)?; assert_eq!(buf, [0xbe, 0xef]); ``` ### hex::encode ```rust pub fn encode(bytes: &[u8]) -> String ``` Encodes `bytes` as lowercase hexadecimal. #### Arguments - `bytes` - The bytes to encode. #### Examples ```rust use helpers4::hex::encode; assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef"); assert_eq!(encode(&[]), ""); ``` ### hex::encode_upper ```rust pub fn encode_upper(bytes: &[u8]) -> String ``` Encodes `bytes` as uppercase hexadecimal. #### Arguments - `bytes` - The bytes to encode. #### Examples ```rust use helpers4::hex::encode_upper; assert_eq!(encode_upper(&[0xde, 0xad, 0xbe, 0xef]), "DEADBEEF"); ``` ### hex::DecodeError ```rust #[non_exhaustive] pub enum DecodeError { /// The string has an odd number of characters. OddLength, /// The string does not match the requested output size. InvalidLength { /// Expected number of hex characters (twice the output size). expected: usize, /// Actual number of hex characters. actual: usize, }, /// A character that is not a hexadecimal digit. InvalidChar { /// Byte offset of the character in the input. index: usize, /// The offending character. found: char, }, } ``` Why a string could not be decoded as hexadecimal. ## Module `http` (Cargo feature `http`) HTTP header value helpers on plain text: no dependency on an HTTP crate. ### http::bearer_token ```rust pub fn bearer_token(header: &str) -> Option<&str> ``` Extracts the token from an `Authorization: Bearer ` header value. Follows RFC 7235 and RFC 6750: the scheme name is case-insensitive, it is followed by one or more spaces, and the token is a non-empty `b64token` (letters, digits and `-` `.` `_` `~` `+` `/`, optionally ending with `=` padding). Leading and trailing spaces or tabs around the whole value are ignored. Anything else, including another scheme or an empty token, gives `None`. Only the syntax is checked: whether the token is valid is up to the caller. #### Arguments - `header` - The value of an `Authorization` header. #### Examples ```rust use helpers4::http::bearer_token; assert_eq!(bearer_token("Bearer abc.def-123"), Some("abc.def-123")); assert_eq!(bearer_token("bearer abc"), Some("abc")); assert_eq!(bearer_token("Basic dXNlcjpwYXNz"), None); assert_eq!(bearer_token("Bearer "), None); ``` ## Module `iter` (Cargo feature `iter`) Helpers for any `Iterator`, not only slices: work on a lazy, single-use or unbounded source. ### iter::chunk ```rust pub fn chunk(iter: I, size: usize) -> Vec> ``` Splits `iter` into consecutive chunks of `size` items, the last one possibly shorter. Unlike [`slice::chunks`](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks), this consumes any `IntoIterator`, not just a slice, and owns the items instead of borrowing them, so it also works on a lazily-generated or single-use iterator. A `size` of `0` produces no chunks at all, since a non-empty chunk cannot hold zero items. #### Arguments - `iter` - The items to split. - `size` - How many items go in each chunk. #### Returns The chunks, in order. #### Examples ```rust use helpers4::iter::chunk; assert_eq!(chunk(1..=5, 2), vec![vec![1, 2], vec![3, 4], vec![5]]); assert_eq!(chunk(Vec::::new(), 3), Vec::>::new()); assert_eq!(chunk(1..=3, 0), Vec::>::new()); ``` ### iter::first_duplicate ```rust pub fn first_duplicate(iter: impl IntoIterator) -> Option ``` Returns the first item of `iter` that has already appeared earlier in it, or `None` when every item is unique. Unlike `array::duplicates`, this stops at the first repeat, so it works on an infinite or otherwise unbounded iterator instead of requiring an already-collected slice. #### Arguments - `iter` - The items to scan, in order. #### Returns The first repeated item, or `None` when there is none. #### Examples ```rust use helpers4::iter::first_duplicate; assert_eq!(first_duplicate([1, 2, 3, 2, 1]), Some(2)); assert_eq!(first_duplicate(["a", "b", "c"]), None); ``` ### iter::min_max ```rust pub fn min_max(iter: impl IntoIterator) -> Option<(T, T)> ``` Returns the smallest and largest item of `iter` in one pass, or `None` when it is empty. Equivalent to calling [`.min()`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.min) and [`.max()`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.max) separately, but only iterates once, so it also works on an iterator that can only be consumed a single time. Comparisons follow `T`'s `PartialOrd`: with `f64`, a `NaN` is neither smaller nor larger than anything, exactly as `<` and `>` say, so a `NaN` that is never replaced (for instance the very first item) stays in the result. #### Arguments - `iter` - The items to scan. #### Returns `(min, max)`, or `None` when `iter` is empty. #### Examples ```rust use helpers4::iter::min_max; assert_eq!(min_max(1..=5), Some((1, 5))); assert_eq!(min_max([3, 1, 4, 1, 5]), Some((1, 5))); assert_eq!(min_max(Vec::::new()), None); ``` ## Module `map` (Cargo feature `map`) Helpers for `HashMap` that the standard library does not provide. Inputs are borrowed and results are new maps: nothing is mutated. ### map::map_values ```rust pub fn map_values( map: &HashMap, mut f: impl FnMut(&V) -> W, ) -> HashMap ``` Returns a new map with the same keys and each value replaced by `f(value)`. #### Arguments - `map` - The map to transform. - `f` - Computes the new value from each old value. #### Examples ```rust use helpers4::map::map_values; use std::collections::HashMap; let prices = HashMap::from([("apple", 2), ("pear", 3)]); let doubled = map_values(&prices, |price| price * 2); assert_eq!(doubled, HashMap::from([("apple", 4), ("pear", 6)])); ``` ### map::omit ```rust pub fn omit( map: &HashMap, keys: &[K], ) -> HashMap ``` Returns a new map with the entries of `map` except those whose key is in `keys`. Keys that are not in `map` are ignored. The complement of `pick`. #### Arguments - `map` - The map to filter. - `keys` - The keys to leave out. #### Examples ```rust use helpers4::map::omit; use std::collections::HashMap; let user = HashMap::from([("name", 1), ("email", 2), ("password", 3)]); assert_eq!(omit(&user, &["password"]), HashMap::from([("name", 1), ("email", 2)])); ``` ### map::pick ```rust pub fn pick( map: &HashMap, keys: &[K], ) -> HashMap ``` Returns a new map with only the entries of `map` whose key is in `keys`. Keys that are not in `map` are ignored. #### Arguments - `map` - The map to filter. - `keys` - The keys to keep. #### Examples ```rust use helpers4::map::pick; use std::collections::HashMap; let user = HashMap::from([("name", 1), ("email", 2), ("password", 3)]); let public = pick(&user, &["name", "email", "age"]); assert_eq!(public, HashMap::from([("name", 1), ("email", 2)])); ``` ## Module `net` (Cargo feature `net`) Network helpers on `std::net` and plain text: no I/O, no resolution. ### net::is_public_ip ```rust pub fn is_public_ip(ip: IpAddr) -> bool ``` Returns `true` when `ip` is a globally reachable unicast address, `false` for everything that is loopback, private, link-local, shared, documentation, reserved or otherwise not on the public internet. It is meant as the address check of an SSRF guard (a server that fetches user-supplied URLs). The guarantee is one-sided: **no address that the IANA special-purpose registries mark as not globally reachable is reported public**. Where a block is refused as a whole and the registry carves a reachable piece out of it, the whole block is refused on purpose. - **IPv4** is refused when it falls in any block of the IANA IPv4 special-purpose registry that is not globally reachable: `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10` (shared address space, e.g. carrier-grade NAT and Tailscale), `127.0.0.0/8`, `169.254.0.0/16` (link-local, including the cloud metadata address), `172.16.0.0/12`, `192.0.0.0/24` (including the two anycast addresses `192.0.0.9` and `192.0.0.10` that the registry lists as reachable), `192.0.2.0/24`, `192.88.99.0/24`, `192.168.0.0/16`, `198.18.0.0/15`, `198.51.100.0/24`, `203.0.113.0/24`, and `224.0.0.0/3` (multicast, reserved and broadcast). - **IPv6** is only accepted inside the global unicast space `2000::/3`, minus `2001::/23` (IETF assignments, including Teredo), `2001:db8::/32` and `3fff::/20` (documentation, RFC 9637) and `2002::/16` (6to4). Loopback, unspecified, unique-local, link-local, multicast and the deprecated IPv4-compatible `::a.b.c.d` form are all outside `2000::/3`. Everything else inside `2000::/3` is accepted, **including blocks IANA holds in reserve** (`2d00::/8` to `3e00::/8`, `3f00::/9` to `3ffe::/16`): nothing is routed there today, and IANA keeps allocating out of this space, so a block delegated tomorrow must not start failing the guard. - **Embedded IPv4** is judged by the IPv4 rules: an IPv4-mapped address (`::ffff:a.b.c.d`) and an address under the NAT64 well-known prefix (`64:ff9b::/96`) are public exactly when the IPv4 address inside is. 6to4 addresses are refused outright. #### What this check cannot know This is only one half of an SSRF defence. - **The address that is really used.** Resolve the name once, validate that address, and connect to it (not to the name again, which invites DNS rebinding), and validate every redirect target the same way. - **What is not an IP literal to `std`.** Spellings such as `2130706433`, `0x7f.1` or `127.1` do not parse as an `IpAddr`, yet a URL parser reads them as `127.0.0.1`. Parse the URL first and pass the address it produces here (see also `is_valid_hostname`, which rejects them). - **Provider-specific addresses inside a public range.** A registry cannot say that, for example, Azure's wire server `168.63.129.16` is internal: it is reported public. #### Arguments - `ip` - The address to check. #### Examples ```rust use helpers4::net::is_public_ip; assert!(is_public_ip("8.8.8.8".parse().unwrap())); assert!(is_public_ip("2606:4700:4700::1111".parse().unwrap())); assert!(!is_public_ip("169.254.169.254".parse().unwrap())); // cloud metadata assert!(!is_public_ip("::1".parse().unwrap())); assert!(!is_public_ip("::ffff:10.0.0.1".parse().unwrap())); // private, in IPv6 clothes ``` ### net::is_valid_hostname ```rust pub fn is_valid_hostname(hostname: &str) -> Result<(), HostnameError> ``` Checks that `hostname` is a valid hostname (RFC 1035 and RFC 1123, ASCII only). The rules: at most 253 octets, not counting one optional trailing dot; labels of 1 to 63 octets made of ASCII letters, digits and hyphens; no label starts or ends with a hyphen. A label may start with a digit, but the **last** label may not be a number (decimal like `1`, or hexadecimal like `0x7f`): RFC 1123 section 2.1 keeps the top-level label alphabetic so that a hostname is never mistaken for an address, and URL parsers do read `127.1`, `2130706433` or `0x7f000001` as `127.0.0.1`. Underscores are refused (they are not hostname characters), and so are non-ASCII characters: convert an internationalized name to its `xn--` form first. This checks syntax only. It is not an SSRF check: a name that passes can still resolve to a private address. To guard a server that fetches user-supplied URLs, parse the URL first, then check the address you will actually connect to with `is_public_ip`. #### Arguments - `hostname` - The hostname to validate. #### Errors A `HostnameError` naming the first rule that fails. #### Examples ```rust use helpers4::net::{is_valid_hostname, HostnameError}; assert!(is_valid_hostname("example.com").is_ok()); assert!(is_valid_hostname("localhost.").is_ok()); assert_eq!(is_valid_hostname("-bad.example"), Err(HostnameError::HyphenEdge)); assert_eq!(is_valid_hostname("a..b"), Err(HostnameError::EmptyLabel)); assert_eq!(is_valid_hostname("127.0.0.1"), Err(HostnameError::NumericLastLabel)); ``` ### net::HostnameError ```rust #[non_exhaustive] pub enum HostnameError { /// The string is empty. Empty, /// The name is longer than 253 octets (not counting an optional trailing dot). TooLong, /// A label is empty: a leading dot, two consecutive dots, or a lone `"."`. EmptyLabel, /// A label is longer than 63 octets. LabelTooLong, /// A character other than an ASCII letter, digit or hyphen. InvalidChar { /// Byte offset of the character in the input. index: usize, /// The offending character. found: char, }, /// A label starts or ends with a hyphen. HyphenEdge, /// The last label is a number (`127.1`, `2130706433`, `0x7f`): URL parsers read such a name as /// an IPv4 address, not as a hostname. NumericLastLabel, } ``` Why a string is not a valid hostname (see `is_valid_hostname`). ## Module `number` (Cargo feature `number`) Numeric helpers that the standard library does not provide. Floating-point helpers never panic: an input with no meaningful answer (an empty slice, a zero total, a `NaN`) gives `None`, and `NaN` or infinite values propagate as usual for `f64`. ### number::gcd ```rust pub fn gcd(mut a: u64, mut b: u64) -> u64 ``` Greatest common divisor of `a` and `b`; `gcd(0, 0)` is `0`. #### Arguments - `a` - The first number. - `b` - The second number. #### Examples ```rust use helpers4::number::gcd; assert_eq!(gcd(12, 18), 6); assert_eq!(gcd(7, 13), 1); assert_eq!(gcd(0, 5), 5); ``` ### number::lcm ```rust pub fn lcm(a: u64, b: u64) -> Option ``` Least common multiple of `a` and `b`, or `None` when it does not fit in a `u64`. `lcm(0, n)` is `0`. #### Arguments - `a` - The first number. - `b` - The second number. #### Examples ```rust use helpers4::number::lcm; assert_eq!(lcm(4, 6), Some(12)); assert_eq!(lcm(0, 5), Some(0)); assert_eq!(lcm(u64::MAX, u64::MAX - 1), None); ``` ### number::lerp ```rust pub fn lerp(from: f64, to: f64, t: f64) -> f64 ``` Linear interpolation between `from` and `to`: `from` at `t = 0`, `to` at `t = 1`. `t` is not clamped, so values outside `0..=1` extrapolate. Written as `from * (1 - t) + to * t`, it is exact at both ends. #### Arguments - `from` - The value at `t = 0`. - `to` - The value at `t = 1`. - `t` - How far to interpolate between `from` and `to`. #### Examples ```rust use helpers4::number::lerp; assert_eq!(lerp(10.0, 20.0, 0.5), 15.0); assert_eq!(lerp(0.0, 100.0, 1.0), 100.0); assert_eq!(lerp(0.0, 10.0, 1.5), 15.0); ``` ### number::mean ```rust pub fn mean(values: &[f64]) -> Option ``` Arithmetic mean of `values`, or `None` when it is empty. `NaN` and infinities propagate as usual for `f64`. #### Arguments - `values` - The values to average. #### Examples ```rust use helpers4::number::mean; assert_eq!(mean(&[1.0, 2.0, 6.0]), Some(3.0)); assert_eq!(mean(&[]), None); ``` ### number::median ```rust pub fn median(values: &[f64]) -> Option ``` Median of `values`, or `None` when it is empty or contains a `NaN`. For an even number of values it is the midpoint of the two middle ones. The input is not modified. #### Arguments - `values` - The values to find the median of. #### Examples ```rust use helpers4::number::median; assert_eq!(median(&[3.0, 1.0, 2.0]), Some(2.0)); assert_eq!(median(&[4.0, 1.0, 3.0, 2.0]), Some(2.5)); assert_eq!(median(&[]), None); ``` ### number::percentage ```rust pub fn percentage(part: f64, total: f64) -> Option ``` What percent `part` is of `total`, or `None` when `total` is zero. The result is not clamped: a `part` larger than `total` gives more than 100. #### Arguments - `part` - The quantity to express as a percentage. - `total` - The whole that `part` is a share of. #### Examples ```rust use helpers4::number::percentage; assert_eq!(percentage(25.0, 200.0), Some(12.5)); assert_eq!(percentage(3.0, 2.0), Some(150.0)); assert_eq!(percentage(1.0, 0.0), None); ``` ### number::round_to ```rust pub fn round_to(value: f64, decimals: u32) -> f64 ``` Rounds `value` to `decimals` decimal places, half away from zero. The result is the nearest `f64` to the rounded decimal, so it can still print with more digits than requested for some values, and binary representation applies: `1.005` is stored as `1.00499999999999989…`, so `round_to(1.005, 2)` is `1.0`. `NaN`, infinities, and values too large to scale are returned unchanged. #### Arguments - `value` - The number to round. - `decimals` - How many decimal places to keep. #### Examples ```rust use helpers4::number::round_to; assert_eq!(round_to(1.23456, 2), 1.23); assert_eq!(round_to(2.5, 0), 3.0); assert_eq!(round_to(-2.5, 0), -3.0); assert_eq!(round_to(1234.0, 0), 1234.0); ``` ## Module `string` (Cargo feature `string`) String manipulation and formatting helpers. ### string::camel_case ```rust pub fn camel_case(s: &str) -> String ``` Converts `s` to `camelCase`. Words are split on any non-alphanumeric character and on case boundaries; an embedded run of capitals is an acronym, so only its last letter starts the next word (`userID` becomes `userId`). #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::camel_case; assert_eq!(camel_case("hello-world"), "helloWorld"); assert_eq!(camel_case("user_name"), "userName"); assert_eq!(camel_case("userID"), "userId"); assert_eq!(camel_case(""), ""); ``` ### string::capitalize ```rust pub fn capitalize(s: &str) -> String ``` Uppercases the first character of `s` and leaves the rest untouched. Unicode-aware: a character whose uppercase form is several characters (`ß` -> `SS`) is expanded accordingly. #### Arguments - `s` - The text to capitalize. #### Examples ```rust use helpers4::string::capitalize; assert_eq!(capitalize("hello world"), "Hello world"); assert_eq!(capitalize(""), ""); ``` ### string::constant_case ```rust pub fn constant_case(s: &str) -> String ``` Converts `s` to `CONSTANT_CASE` (also known as `SCREAMING_SNAKE_CASE`). Splits words the same way as `camel_case`. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::constant_case; assert_eq!(constant_case("helloWorld"), "HELLO_WORLD"); assert_eq!(constant_case("max retries"), "MAX_RETRIES"); assert_eq!(constant_case(""), ""); ``` ### string::dedent ```rust pub fn dedent(s: &str) -> String ``` Strips the indentation shared by every non-blank line of `s`, and drops one leading and one trailing blank line. Lets a multi-line string literal be indented with the surrounding code without that indentation leaking into the value. Indentation is counted in whitespace characters, and lines are split on `'\n'` only (a `'\r'` stays on its line). #### Arguments - `s` - The text to strip the shared indentation from. #### Examples ```rust use helpers4::string::dedent; assert_eq!(dedent("\n Hello\n World\n"), "Hello\n World"); assert_eq!(dedent(" a\n b"), "a\nb"); ``` ### string::escape_html ```rust pub fn escape_html(s: &str) -> Cow<'_, str> ``` Escapes the HTML special characters `&`, `<`, `>`, `"` and `'`. Returns the input borrowed, without allocating, when there is nothing to escape. Use it to embed untrusted text in HTML text nodes or quoted attribute values. #### Arguments - `s` - The text to escape. #### Examples ```rust use helpers4::string::escape_html; assert_eq!( escape_html(""), "<script>alert("xss")</script>" ); assert_eq!(escape_html("It's a & more"), "It's a <test> & more"); assert_eq!(escape_html("plain"), "plain"); ``` ### string::indent ```rust pub fn indent(s: &str, prefix: &str) -> String ``` Prefixes every non-blank line of `s` with `prefix`. Blank lines (empty or whitespace only) are left untouched, so no trailing whitespace is introduced. Lines are split on `'\n'` only, and a trailing newline is preserved. It is the inverse of `dedent` for text indented with a fixed prefix. #### Arguments - `s` - The text to indent. - `prefix` - The text to prepend to every non-blank line. #### Examples ```rust use helpers4::string::indent; assert_eq!(indent("a\n\nb", " "), " a\n\n b"); assert_eq!(indent("line\n", "> "), "> line\n"); ``` ### string::kebab_case ```rust pub fn kebab_case(s: &str) -> String ``` Converts `s` to `kebab-case`. Splits words the same way as `camel_case`. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::kebab_case; assert_eq!(kebab_case("helloWorld"), "hello-world"); assert_eq!(kebab_case("user_name"), "user-name"); assert_eq!(kebab_case(""), ""); ``` ### string::pascal_case ```rust pub fn pascal_case(s: &str) -> String ``` Converts `s` to `PascalCase`. Splits words the same way as `camel_case`. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::pascal_case; assert_eq!(pascal_case("hello-world"), "HelloWorld"); assert_eq!(pascal_case("user_name"), "UserName"); assert_eq!(pascal_case(""), ""); ``` ### string::slugify ```rust pub fn slugify(s: &str) -> String ``` Converts `s` into a lowercase, hyphen-separated slug safe for URLs. Letters and digits (Unicode included) are kept and lowercased, apostrophes are dropped, and every other run of characters becomes a single hyphen; leading and trailing hyphens are never produced. Diacritics are **not** stripped: `"café"` stays `"café"`. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::slugify; assert_eq!(slugify("Hello World!"), "hello-world"); assert_eq!(slugify(" It's a --- test "), "its-a-test"); assert_eq!(slugify("!!!"), ""); ``` ### string::snake_case ```rust pub fn snake_case(s: &str) -> String ``` Converts `s` to `snake_case`. Splits words the same way as `camel_case`. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::snake_case; assert_eq!(snake_case("helloWorld"), "hello_world"); assert_eq!(snake_case("Hello World"), "hello_world"); assert_eq!(snake_case(""), ""); ``` ### string::squish ```rust pub fn squish(s: &str) -> String ``` Trims `s` and collapses every run of whitespace into a single space. Tabs and line breaks count as whitespace, so a multi-line text becomes one line. #### Arguments - `s` - The text to normalize. #### Examples ```rust use helpers4::string::squish; assert_eq!(squish(" hello \n\t world "), "hello world"); assert_eq!(squish(" "), ""); ``` ### string::title_case ```rust pub fn title_case(s: &str) -> String ``` Capitalizes the first letter of every whitespace-separated word and lowercases the rest. Whitespace is kept as it is. Only whitespace starts a new word, so `it's` becomes `It's` and `well-known` becomes `Well-known`. Use `pascal_case` to also drop the separators. #### Arguments - `s` - The text to convert. #### Examples ```rust use helpers4::string::title_case; assert_eq!(title_case("the quick brown fox"), "The Quick Brown Fox"); assert_eq!(title_case("HELLO wORLD"), "Hello World"); assert_eq!(title_case("it's"), "It's"); ``` ### string::truncate ```rust pub fn truncate(s: &str, max_chars: usize, suffix: &str) -> String ``` Shortens `s` to at most `max_chars` characters, ending with `suffix` when it was cut. The suffix counts toward the limit. Lengths are in Unicode scalar values (`char`s), not grapheme clusters. If the suffix alone does not fit, the first `max_chars` characters of the suffix are returned. #### Arguments - `s` - The text to shorten. - `max_chars` - The maximum length of the result, suffix included. - `suffix` - Appended when `s` was cut. #### Examples ```rust use helpers4::string::truncate; assert_eq!(truncate("Hello, world", 8, "..."), "Hello..."); assert_eq!(truncate("short", 8, "..."), "short"); assert_eq!(truncate("Hello", 2, "..."), ".."); ``` ### string::unescape_html ```rust pub fn unescape_html(s: &str) -> Cow<'_, str> ``` Decodes the HTML entities `&`, `<`, `>`, `"`, `'`, `'` and any numeric character reference (`A`, `A`). It is the inverse of `escape_html`. The text is decoded in a single pass, so `&lt;` becomes `<` and not `<`. Anything that is not a known entity, including a numeric reference that is not a valid character, is left as it is. Returns the input borrowed, without allocating, when it contains no `&`. #### Arguments - `s` - The text to decode. #### Examples ```rust use helpers4::string::unescape_html; assert_eq!(unescape_html("<b>Tom & Jerry</b>"), "Tom & Jerry"); assert_eq!(unescape_html("AB"), "AB"); assert_eq!(unescape_html("&lt;"), "<"); assert_eq!(unescape_html("&unknown;"), "&unknown;"); ``` ## Module `time` (Cargo feature `time`) Time helpers. Reading the clock is explicit and fallible: a clock set before 1970 is an error, not `0`. ### time::unix_now ```rust pub fn unix_now() -> Result ``` The current time as whole seconds since the Unix epoch. #### Errors `ClockError` when the system clock is set before 1970. Do not turn that into `0`: a token expiry checked against `0` would look valid forever. #### Examples ```rust use helpers4::time::unix_now; let now = unix_now()?; assert!(now > 1_700_000_000); // after November 2023 ``` ### time::unix_now_millis ```rust pub fn unix_now_millis() -> Result ``` The current time as milliseconds since the Unix epoch. The value saturates at `u64::MAX`, which is some 584 million years away. #### Errors `ClockError` when the system clock is set before 1970 (see `unix_now`). #### Examples ```rust use helpers4::time::unix_now_millis; assert!(unix_now_millis()? > 1_700_000_000_000); ``` ### time::ClockError ```rust pub struct ClockError { /* private fields */ } ``` The system clock is set before the Unix epoch (1970-01-01T00:00:00Z). Returned instead of a silent `0`: an expiry comparison against `0` would treat every token as still valid. #### ClockError::behind ```rust pub fn behind(&self) -> Duration ``` How far before the epoch the clock is. ##### Returns How far in the past the system clock reported, relative to the Unix epoch. ## Module `validate` (Cargo feature `validate`) Shape checks for common user-facing formats: pragmatic subsets that catch real mistakes, not full grammars. None of them normalizes or parses the value, only checks it. ### validate::is_slug ```rust pub fn is_slug(s: &str) -> bool ``` Checks whether `s` has the shape of an ASCII URL slug: non-empty, made only of lowercase ASCII letters, digits and hyphens, with no leading, trailing or doubled hyphen. This is ASCII-only by design, even though `slugify` keeps Unicode letters (`slugify("café")` is `"café"`, which `is_slug` rejects): a slug meant to go unescaped in a URL path is conventionally ASCII. For ASCII input, `is_slug(&slugify(s))` is `true` whenever `slugify(s)` is not empty. #### Arguments - `s` - The text to check. #### Returns `true` when `s` already has the shape of a slug. #### Examples ```rust use helpers4::validate::is_slug; assert!(is_slug("hello-world")); assert!(is_slug("v2")); assert!(!is_slug("Hello-World")); assert!(!is_slug("-leading")); assert!(!is_slug("double--hyphen")); assert!(!is_slug("")); ``` ### validate::is_uuid ```rust pub fn is_uuid(s: &str) -> bool ``` Checks whether `s` is a UUID in its canonical `8-4-4-4-12` hyphenated hexadecimal form. Case-insensitive. The variant and version digits are not checked, so this accepts any RFC 9562 UUID (v1 through v8) as well as the all-zero nil UUID; it only checks the shape. #### Arguments - `s` - The text to check. #### Returns `true` when `s` has the shape of a UUID. #### Examples ```rust use helpers4::validate::is_uuid; assert!(is_uuid("550e8400-e29b-41d4-a716-446655440000")); assert!(is_uuid("550E8400-E29B-41D4-A716-446655440000")); assert!(!is_uuid("550e8400-e29b-41d4-a716-44665544000")); // one digit short assert!(!is_uuid("not-a-uuid")); ``` ### validate::is_valid_email ```rust pub fn is_valid_email(s: &str) -> bool ``` Checks a pragmatic subset of RFC 5322 that catches real typos, not a full grammar. Requires exactly one `@`, a non-empty local part of ASCII letters, digits and `. _ % + -` with no leading, trailing or doubled dot, and a domain of at least two dot-separated labels (ASCII letters, digits and hyphens, none starting or ending with a hyphen) whose last label is letters only. Quoted local parts, comments and internationalized domain names are not supported: this is meant to reject obvious mistakes, not to be the final word on deliverability. #### Arguments - `s` - The address to check. #### Returns `true` when `s` passes the checks above. #### Examples ```rust use helpers4::validate::is_valid_email; assert!(is_valid_email("jane.doe+list@example.co.uk")); assert!(!is_valid_email("no-at-sign")); assert!(!is_valid_email("@example.com")); assert!(!is_valid_email("jane@localhost")); ```