# 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`](https://github.com/imcooder/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).
[](https://github.com/imcooder/bento-kit-rs/actions/workflows/ci.yml)
[](https://crates.io/crates/bento-kit)
[](https://docs.rs/bento-kit)
## 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
| `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
```toml
# default features (id + time + mask)
bento-kit = "0.1"
# only what you need
bento-kit = { version = "0.1", default-features = false, features = ["id"] }
```
```rust
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
| `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.ts` → `bento_kit::time::TimeUse`)
| `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)
| `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`
```rust
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`.
```rust
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:
```rust
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`
```rust
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
```bash
cargo run --example id_demo
cargo run --example time_demo
cargo run --example mask_demo
```
## Quality
```bash
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:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.