# fluent-iterable (Rust)
> The [fluent-iterable](https://github.com/codibre/fluent-iterable)
> vocabulary for Rust — an extension trait over `std::iter::Iterator`,
> published on crates.io as [`fluent-iterable`].
JavaScript needed fluent iterator chains, and this monorepo shipped them.
Rust's `std` already owns most of that surface (`map`, `filter`,
`flat_map`, `any`, `fold`, …) — but not the parts with real algorithmic
content: stable grouping, equi-joins, distinctness checks, cardinality
short-circuits, nested indices. This crate ports exactly that residue, in
the same operation names, as one extension trait:
```rust
use fluent_iterable::FluentIter;
let orders = [(1, 100), (2, 200), (1, 50)];
let customers = [(1, "Ada"), (2, "Grace")];
// inner join, like .combine(other, keySelf, keyOther) — lazy: the left
// side streams, resolve with collect()
let lines: Vec<(&i32, &(i32, &str))> = orders
.iter()
.combine(customers.iter(), |(o, _)| *o, |(id, _)| *id)
.collect();
// stable grouping, like .group(keyFn) — resolves to Vec (the pass is
// inherently full: any item can extend an earlier group)
let report = orders
.iter()
.map(|(_, v)| *v)
.reduce_and_map(0, |acc, v| acc + v, |sum| format!("total {sum}"));
assert_eq!(report, "total 350");
```
No wrapper type, no runtime dependencies, nothing to learn beyond your
iterators: `use fluent_iterable::FluentIter;` and chain. Laziness is
applied where it pays: `distinct`/`distinct_by`/`combine` are streaming
adapters that short-circuit under `take(k)`; operations whose first result
is not final until the whole source is read (`group_by_stable`,
`combine_join`, the sort family) resolve to a solid `Vec` — same work, same
memory, plus re-iterability and every `Vec` method for free.
## npm ⇄ Rust map
Operations this crate provides (npm name → Rust method):
| `.group(keyFn)` | `group_by_stable` (resolves to `Vec`; the pass is inherently full) |
| `.combine(other, ka, kb)` | `combine` (lazy; left streams) |
| `.combineJoin(baseKey)` | `combine_join` (resolves to `Vec`) |
| `.distinct()` | `distinct` (lazy adapter) |
| `.distinct(keyFn)` | `distinct_by` (lazy adapter) |
| `.isDistinct()` | `is_distinct` (short-circuits) |
| `.isDistinct(keyFn)` | `is_distinct_by` (short-circuits) |
| `.toMap(k, v)` | `to_map_by` |
| `.toMapChain(k1, k2)` | `to_map_chain` (depth 2) |
| `.toMapChainReduce(…)` | `to_map_chain_reduce` (depth 2) |
| `.join(sep)` | `join` |
| `.join(sep, mapFn)` | `join_by` |
| `.avg()` / `.avg(mapFn)` | `avg` / `avg_by` (`Option<f64>`, `None` empty) |
| `.sort(cmp?)` | `sorted` / `sort_with` (stable, resolves to `Vec`) |
| `.sortBy(key)` / desc | `sort_by` / `sort_by_desc` (stable, resolves to `Vec`) |
| `.hasExactly(n)` | `has_exactly` |
| `.hasLessThan(n)` | `has_less_than` |
| `.hasLessOrExactly(n)` | `has_less_or_exactly` |
| `.hasMoreThan(n)` | `has_more_than` |
| `.hasMoreOrExactly(n)` | `has_more_or_exactly` |
| `.reduceAndMap(r, i, m)` | `reduce_and_map` (init first, Rust-style) |
| `.contains(item)` | `contains` |
| `.whenEmpty(fb)` | `when_empty` (lazy adapter) |
| `.repeat(n)` | `repeat` (finite cycle, lazy adapter) |
Operations you do **not** need here — `std` already has them:
`map`, `filter`, `filter_map`, `flat_map`/`flatten`, `take`, `skip`,
`take_while`, `skip_while`, `count`, `any`/`all`, `sum`, `min`, `max`,
`fold` (reduce), `chain` (concat/append), `enumerate` (withIndex),
`collect` (toArray), `slice::chunks` (partition), `max_by_key` (top).
## What is deliberately not ported
| every `*Async` operation, `flatMerge` | stable Rust has no `AsyncIterator` trait; revisit when the language lands it. |
| `emit` / `catch` / `finally` / `execute` / `waitAll` | JS emitter/lifecycle surface; `Result`, `Drop` and stream crates own this in Rust. |
| `toObject` / `toObjectChain*` / `unwind` | dynamic-property idioms; struct selection via `flat_map` is the Rust answer. |
| `branch` | needs fan-out buffering (`tee`); candidate for v2. |
| `distinct(key, choose, maxOcurrences)` extras | v1 is first-wins; a chooser/max-occurrences flavor can land later. |
| `toMapChain` arbitrary key depth | Rust fixes the concrete nested type at depth 2; deeper nesting folds layers. |
Intentional semantics upgrades are marked in the [rustdoc](https://docs.rs/fluent-iterable)
(`avg` returning `Option<f64>` instead of `NaN`, stable sorts by `Ord`
instead of JS string coercion, explicit initial value for
`to_map_chain_reduce`).
## Development (inside this monorepo)
This directory is both a cargo package and a pnpm workspace member, so the
usual root commands drive it:
```sh
pnpm install # registers the member
pnpm --filter fluent-iterable-rs test # cargo test
pnpm --filter fluent-iterable-rs lint # cargo fmt --check + clippy -D warnings
turbo run test --filter=fluent-iterable-rs # via turbo, with caching
cargo test # or plain cargo, inside this dir
```
Coverage locally (not wired into the turbo `test:coverage` task on purpose —
CI runners have no `cargo-llvm-cov`):
```sh
cargo install cargo-llvm-cov
cargo llvm-cov --all-targets --lcov --output-path lcov.info
```
## Releasing
The crate releases itself the same way the TS siblings do: **conventional
commits touching this directory** are the release trigger. On merge to
`master`, the publish pipeline runs release-it here — the shared
`.release-it.base.js` gate looks for `feat`/`fix` commits under this path
since the last `fluent-iterable-rs@<v>` tag (silent skip otherwise), the
conventional preset picks the next version (feat→minor, fix→patch on 0.x),
and the `.release-it.js` hook (`scripts/cargo-publish.sh`) mirrors the new
version into `Cargo.toml` and publishes the crate to crates.io. The same
release-it run commits and tags — so the crate shows up in the workflow's
Published/Skipped summary alongside the npm packages.
Auth is **trusted publishing (OIDC)**: `rust-lang/crates-io-auth-action`
exchanges the workflow identity for a 30-minute crates.io token — no API
token stored anywhere. One-time setup (crate owner, on crates.io): crate
**Settings → Trusted Publishing → Add (GitHub)**, owner `codibre`,
repository `fluent-iterable`, workflow filename `publish.yaml`, environment
empty.
Manual escape hatch: `cargo publish` from this directory (needs `cargo
login` locally); `pnpm run publish:check` / `publish:dry` preview state
against crates.io, `version:set` force-syncs both manifests.
[`fluent-iterable`]: https://crates.io/crates/fluent-iterable