bento-kit 0.1.1

A bento box of common Rust utilities: id generation, timing, masking
Documentation

bento-kit

A bento box of common Rust utilities, packaged as one crate with cargo features. This is the Rust port of du-node-utils; the public API tracks the Node version function-for-function so projects in either ecosystem can speak the same vocabulary (same UID format, same session-ID layout, same masking semantics).

CI crates.io docs.rs

Design

  • One crate, many modules. Add a single dependency, opt into modules via cargo features. No workspace sprawl.
  • Node parity first. Every Node function from du-node-utils has a Rust counterpart with matching behavior. Rust-specific extensions (UUID v7, nanoid) are clearly labeled.
  • Idiomatic Rust on top. Module paths and naming follow Rust conventions (snake_case, Option<&str> for optional args, Result for fallible ops). The behavior is identical; only the spelling changes.

Modules

feature what you get default
id UUID v4/v7, nanoid, session/user/db-key IDs yes
time Multi-tag profiler (TimeUse) + timestamp formatting yes
mask Sensitive-data masking (phone, email, token, secret …) yes
full Alias for all of the above no

Install

# default features (id + time + mask)
bento-kit = "0.1"

# only what you need
bento-kit = { version = "0.1", default-features = false, features = ["id"] }
use bento_kit::{id, mask, time};

Node-to-Rust API map

The du-node-utils lib exports JS functions in camelCase; their Rust equivalents are listed below. Import paths are relative to the crate root.

id module

Node Rust
id.makeUUID(false) [id::uuid_v4]
id.makeUUID(true) [id::uuid_v4_simple]
id.randomInt(min, max) [id::random_int]
id.randomString(len) [id::random_string]
id.makeUidPostfix() [id::make_uid_postfix]
id.makeUserId(appid, uid, cuid) [id::make_user_id]
id.parseUserid(s) [id::parse_user_id] → Option<UserId>
id.makeDbKey(uid) [id::make_db_key]
id.setSessionPrefix(p) [id::set_session_prefix]
id.generateSessionId() [id::generate_session_id]
[id::uuid_v7] / [id::uuid_v7_simple]
[id::nanoid] / [id::nanoid_with_len] / [id::nanoid_with_alphabet]

time module (Node lib/TimeUse.tsbento_kit::time::TimeUse)

Node XTimeUse method Rust TimeUse method
new XTimeUse() [time::TimeUse::new]
start(tag?) start(tag: Option<&str>)
stop(tag?) stop(tag: Option<&str>) -> Duration (idempotent)
restart(tag?) restart(tag: Option<&str>) -> Duration
elapsed(tag?) elapsed(tag: Option<&str>) -> Duration (peek)
get(tag?) intentionally not ported — see below

Plus timestamp helpers not in the Node lib: [time::now_millis], [time::now_seconds], [time::now_micros], [time::format_now_utc], [time::format_now_local], [time::format_timestamp_utc], [time::format_timestamp_local].

get(tag?) is intentionally omitted. The Node version returns a Unix millisecond timestamp; in Rust the underlying Instant is monotonic and not directly convertible to wall-clock without surrendering its correctness guarantees. Use [time::now_millis] if a wall-clock timestamp is what you actually need.

mask module (Node lib/MaskSecret.ts + du-node-utils per-field helpers)

Node Rust
maskSecret(value, keepChars=3) [mask::mask_secret](value: Option<&str>, keep_chars: usize)
(Rust addition) [mask::mask_phone] / [mask::mask_email] / [mask::mask_id_card] / [mask::mask_bank_card] / [mask::mask_token] / [mask::mask_name] / [mask::mask_middle]

Quick tour

id

use bento_kit::id;

// UUIDs
let v4 = id::uuid_v4();             // 36 chars, hyphenated
let v4s = id::uuid_v4_simple();     // 32 hex chars, no hyphens
let v7 = id::uuid_v7();             // time-ordered, monotonic per ms

// Random helpers (not crypto-secure — same trade-off as Node's Math.random)
let n = id::random_int(0, 100);     // half-open [0, 100)
let s = id::random_string(20);      // base36 keyboard-order alphabet

// Session and user IDs (Node-compatible format)
id::set_session_prefix("myapp");
let sid = id::generate_session_id();   // "myapp1735088400123x9y8z"

let user = id::make_user_id("app1", "user42", "client99");
// "connect.app1.user42.client99"
let parsed = id::parse_user_id(&user).unwrap();
assert_eq!(parsed.uid, "user42");

let db = id::make_db_key("user42");    // "connect.user42"
let post = id::make_uid_postfix();     // hex of (ms*1000 + rand)

// Rust-side conveniences (no Node equivalent)
let n1 = id::nanoid();                  // 21-char URL-safe
let n2 = id::nanoid_with_len(8);

time

TimeUse is a multi-stage profiler. Every method takes tag: Option<&str>None operates on the global timer started at TimeUse::new, Some("foo") on a per-tag span tracked in a HashMap.

use bento_kit::time::TimeUse;

let mut t = TimeUse::new();

t.start(Some("connect"));
// ... establish connection ...
let connect = t.stop(Some("connect"));

t.start(Some("query"));
// ... run query ...
let query = t.stop(Some("query"));

let total = t.stop(None);

Two semantic points worth knowing:

  • Idempotent stop. Once a span has been stopped, subsequent stop / elapsed calls return the cached duration. You won't accidentally double-charge a region.
  • Tag fallback. stop(Some("foo")) on a tag that was never started uses the global start as the origin, matching the Node behavior.

Timestamp helpers:

use bento_kit::time::{now_millis, format_now_utc, format_timestamp_utc};

let ms = now_millis();
let iso = format_now_utc("%Y-%m-%dT%H:%M:%SZ");
let s = format_timestamp_utc(1_704_067_200, "%Y-%m-%d").unwrap();

mask

use bento_kit::mask;

mask::mask_phone("13812345678");                   // "138****5678"
mask::mask_email("alice@example.com");             // "a***@example.com"
mask::mask_id_card("110101199001011234");          // "110101********1234"
mask::mask_bank_card("6225881234567890");          // "6225********7890"
mask::mask_token("sk-1234567890abcdef");           // "sk-1***********cdef"
mask::mask_name("张三丰");                          // "张*丰"
mask::mask_middle("custom-string", 2, 2);          // "cu*********ng"

// Direct port of du-node-utils.maskSecret:
mask::mask_secret(Some("vault:AES256:abcXYZ"), 3); // "vau***XYZ"
mask::mask_secret(Some("ab"), 3);                  // "ab"   (too short to mask)
mask::mask_secret(None, 3);                        // "<none>"
mask::mask_secret(Some(""), 3);                    // "<empty>"

mask_secret mirrors the Node version exactly: when the input is long enough, it always uses three stars as the divider regardless of the masked length, and returns sentinel strings for None / "".

Examples

cargo run --example id_demo
cargo run --example time_demo
cargo run --example mask_demo

Quality

cargo test --all-features
cargo clippy --all-features --all-targets -- -D warnings
cargo fmt --all -- --check

CI runs the full test matrix on Linux, macOS, and Windows against both stable Rust and the declared MSRV (1.74), plus a feature matrix that exercises each feature in isolation.

MSRV

Rust 1.74. Bumping the MSRV is treated as a minor version change.

Releasing

Releases are tag-driven. Pushing a tag of the form v1.2.3 triggers the release workflow, which:

  1. Validates the tag is a valid semver string.
  2. Stamps 1.2.3 into Cargo.toml via cargo set-version.
  3. Runs the full test suite.
  4. Calls cargo publish with the CARGO_REGISTRY_TOKEN secret.

You do not need to manually bump the version in Cargo.toml before tagging — the local value is a development placeholder.

License

Dual-licensed under either of:

at your option.