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`](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).

[![CI](https://github.com/imcooder/bento-kit-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/imcooder/bento-kit-rs/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/bento-kit.svg)](https://crates.io/crates/bento-kit)
[![docs.rs](https://docs.rs/bento-kit/badge.svg)](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

| 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

```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

| 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.ts``bento_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`

```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.