Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dc52a0d
chore: rustfmt
bnomei Jan 7, 2026
8dbf842
fix(types): add safe get methods to JsonValue
bnomei Jan 7, 2026
e69c69d
fix(scanner): replace unwrap with safe option handling
bnomei Jan 7, 2026
2907109
fix(expansion): use expect with descriptive message
bnomei Jan 7, 2026
32dfc85
fix(encode): use expect for field list lookup
bnomei Jan 7, 2026
518e15c
test: add panic safety tests
bnomei Jan 7, 2026
08f5d35
fix(types): avoid borrow overlap in IndexMut
bnomei Jan 7, 2026
31f75b1
test: fix panic safety import
bnomei Jan 7, 2026
071a27c
fix(scanner): satisfy clippy
bnomei Jan 7, 2026
67aca3c
fix(decoder): improve decode correctness
bnomei Jan 7, 2026
bde58a8
test: add decoder edge cases
bnomei Jan 7, 2026
085e466
fix(strict): enforce strict validations and relax non-strict
bnomei Jan 7, 2026
789c11d
chore: split cli/tui feature flags
bnomei Jan 7, 2026
3ad150d
perf: throughput pass 1
bnomei Jan 7, 2026
f5b1b7e
fix: preserve large integer formatting
bnomei Jan 7, 2026
56d9f4b
docs: refresh performance snapshot
bnomei Jan 7, 2026
64b8e88
test: add fuzzing and boundary coverage
bnomei Jan 7, 2026
72e651b
docs: document fuzzing workflow
bnomei Jan 7, 2026
7c66bee
test: expand helper and value coverage
bnomei Jan 7, 2026
0ff0274
test: cover parser and scanner errors
bnomei Jan 7, 2026
c94aca7
test: fix clippy warnings in encode helpers
bnomei Jan 7, 2026
9963e62
feat(serde): add serde-style api
bnomei Jan 7, 2026
d23474f
docs: refresh documentation and examples
bnomei Jan 7, 2026
a823915
docs: update cli help text
bnomei Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ coverage/
tarpaulin-report.html
cobertura.xml

# Fuzzing
fuzz/target/
fuzz/corpus/
fuzz/artifacts/

# Rust
target/
Cargo.lock
Expand Down
35 changes: 17 additions & 18 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,46 +25,45 @@ path = "src/cli/main.rs"
required-features = ["cli"]

[features]
default = ["cli"]
cli = [
"dep:clap",
"dep:anyhow",
"dep:tiktoken-rs",
"dep:comfy-table",
"dep:ratatui",
"dep:crossterm",
"dep:tui-textarea",
"dep:arboard",
"dep:syntect",
"dep:unicode-width",
"dep:chrono",
]
default = ["cli", "cli-stats", "tui", "tui-clipboard", "tui-time", "parallel"]
cli = ["dep:clap", "dep:anyhow"]
cli-stats = ["cli", "dep:tiktoken-rs", "dep:comfy-table"]
tui = ["dep:anyhow", "dep:ratatui", "dep:crossterm", "dep:tui-textarea"]
tui-clipboard = ["tui", "dep:arboard"]
tui-time = ["tui", "dep:chrono"]
parallel = ["dep:rayon"]

[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
indexmap = "2.0"
serde_json = { version = "1.0.145", features = ["preserve_order"] }
thiserror = "2.0.17"
itoa = "1.0"
ryu = "1.0"
rayon = { version = "1.10", optional = true }

# CLI dependencies (gated behind "cli" feature)
# CLI dependencies (gated behind "cli"/"cli-stats" features)
clap = { version = "4.5.11", features = ["derive"], optional = true }
anyhow = { version = "1.0.86", optional = true }
tiktoken-rs = { version = "0.9.1", optional = true }
comfy-table = { version = "7.1", optional = true }

# TUI dependencies (gated behind "cli" feature)
# TUI dependencies (gated behind "tui" feature)
ratatui = { version = "0.29", optional = true }
crossterm = { version = "0.28", optional = true }
tui-textarea = { version = "0.7", optional = true }
arboard = { version = "3.4", optional = true }
syntect = { version = "5.2", optional = true }
unicode-width = { version = "0.2", optional = true }
chrono = { version = "0.4", optional = true }

[dev-dependencies]
datatest-stable = "0.3.3"
glob = "0.3"
criterion = "0.5"

[[test]]
name = "spec_fixtures"
harness = false

[[bench]]
name = "encode_decode"
harness = false
134 changes: 118 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

[![Crates.io](https://img.shields.io/crates/v/toon-format.svg)](https://crates.io/crates/toon-format)
[![Documentation](https://docs.rs/toon-format/badge.svg)](https://docs.rs/toon-format)
[![Spec v2.0](https://img.shields.io/badge/spec-v2.0-brightgreen.svg)](https://github.com/toon-format/spec/blob/main/SPEC.md)
[![Spec v3.0](https://img.shields.io/badge/spec-v3.0-brightgreen.svg)](https://github.com/toon-format/spec/blob/main/SPEC.md)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Tests](https://img.shields.io/badge/tests-%20passing-success.svg)]()

**Token-Oriented Object Notation (TOON)** is a compact, human-readable format designed for passing structured data to Large Language Models with significantly reduced token usage.

This crate provides the official, **spec-compliant Rust implementation** of TOON v2.0 with v1.5 optional features, offering both a library (`toon-format`) and a full-featured command-line tool (`toon`).
This crate provides the official, **spec-compliant Rust implementation** of TOON v3.0 with v1.5 optional features, offering both a library (`toon-format`) and a full-featured command-line tool (`toon`).

## Quick Example

Expand All @@ -32,13 +32,32 @@ users[2]{id,name}:
## Features

- **Generic API**: Works with any `Serialize`/`Deserialize` type - custom structs, enums, JSON values, and more
- **Spec-Compliant**: Fully compliant with [TOON Specification v2.0](https://github.com/toon-format/spec/blob/main/SPEC.md)
- **Spec-Compliant**: Fully compliant with [TOON Specification v3.0](https://github.com/toon-format/spec/blob/main/SPEC.md)
- **v1.5 Optional Features**: Key folding and path expansion
- **Safe & Performant**: Built with safe, fast Rust
- **Powerful CLI**: Full-featured command-line tool
- **Strict Validation**: Enforces all spec rules (configurable)
- **Well-Tested**: Comprehensive test suite with unit tests, spec fixtures, and real-world scenarios

## Performance Snapshot (Criterion)

Snapshot from commit `f5b1b7e` using:
`cargo bench --bench encode_decode -- --save-baseline current --noplot`

| Benchmark | Median |
| --- | --- |
| `tabular/encode/128` | 145.81 us |
| `tabular/decode/128` | 115.51 us |
| `tabular/encode/1024` | 1.2059 ms |
| `tabular/decode/1024` | 949.65 us |
| `deep_object/encode/32` | 11.766 us |
| `deep_object/decode/32` | 10.930 us |
| `deep_object/encode/128` | 46.867 us |
| `deep_object/decode/128` | 49.468 us |
| `decode_long_unquoted` | 10.554 us |

Numbers vary by machine; use Criterion baselines to compare before/after changes.

## Installation

### As a Library
Expand All @@ -53,6 +72,22 @@ cargo add toon-format
cargo install toon-format
```

### Feature Flags

By default, all CLI/TUI features are enabled. You can opt in to only what you need:

```toml
toon-format = { version = "0.4", default-features = false }
```

```bash
cargo install toon-format --no-default-features --features cli
cargo install toon-format --no-default-features --features cli,cli-stats
cargo install toon-format --no-default-features --features cli,tui,tui-clipboard,tui-time
```

Feature summary: `cli`, `cli-stats`, `tui`, `tui-clipboard`, `tui-time`, `parallel`.

---

## Library Usage
Expand Down Expand Up @@ -126,6 +161,38 @@ fn main() -> Result<(), toon_format::ToonError> {
Ok(())
}
```

### Serde-Style API

Prefer serde_json-like helpers? Use `to_string`/`from_str` and friends:

```rust
use serde::{Deserialize, Serialize};
use toon_format::{from_reader, from_str, to_string, to_writer};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct User {
name: String,
age: u32,
}

let user = User {
name: "Ada".to_string(),
age: 37,
};

let toon = to_string(&user)?;
let round_trip: User = from_str(&toon)?;

let mut buffer = Vec::new();
to_writer(&mut buffer, &user)?;
let round_trip: User = from_reader(buffer.as_slice())?;
# Ok::<(), toon_format::ToonError>(())
```

Option-aware variants: `to_string_with_options`, `to_writer_with_options`,
`from_str_with_options`, `from_slice_with_options`, `from_reader_with_options`.

---

## API Reference
Expand Down Expand Up @@ -417,6 +484,8 @@ See [docs/TUI.md](docs/TUI.md) for complete documentation and keyboard shortcuts

## CLI Usage

See [docs/CLI.md](docs/CLI.md) for the full option reference and behavior details.

### Basic Commands

```bash
Expand All @@ -428,6 +497,9 @@ toon data.toon # Decode
toon -e data.txt # Force encode
toon -d output.txt # Force decode

# Write to a file
toon input.json -o output.toon

# Pipe from stdin
cat data.json | toon
echo '{"name": "Alice"}' | toon -e
Expand All @@ -447,7 +519,7 @@ toon data.json --indent 4
toon data.json --fold-keys
toon data.json --fold-keys --flatten-depth 2

# Show statistics
# Show statistics (requires cli-stats feature)
toon data.json --stats
```

Expand Down Expand Up @@ -529,25 +601,49 @@ match decode_strict::<Value>("items[3]: a,b") {

---

## Numeric Precision

This implementation handles numbers as follows:

- **Integers**: Values within `i64` range (`-9,223,372,036,854,775,808` to
`9,223,372,036,854,775,807`) are preserved exactly
- **Floating-point**: Numbers outside `i64` range or with decimal points use `f64`,
which provides ~15-17 significant digits of precision
- **Safe integers**: For JavaScript interoperability, integers up to `2^53 - 1`
(9,007,199,254,740,991) are safe

### Special Values

| Input | TOON Output |
|-------|-------------|
| `NaN` | `null` |
| `Infinity` | `null` |
| `-Infinity` | `null` |

These conversions follow the TOON specification requirement that all values must be
JSON-compatible.

---


## Examples
Run with `cargo run --example examples` to see all examples:
- `structs.rs` - Custom struct serialization
- `tabular.rs` - Tabular array formatting
- `arrays.rs` - Various array formats
- `arrays_of_arrays.rs` - Nested arrays
- `objects.rs` - Object encoding
- `mixed_arrays.rs` - Mixed-type arrays
- `delimiters.rs` - Custom delimiters
- `round_trip.rs` - Encode/decode round-trips
- `decode_strict.rs` - Strict validation
- `empty_and_root.rs` - Edge cases
Run a specific example with `cargo run --example <name>`:
- `structs` - Custom struct serialization
- `tabular` - Tabular array formatting
- `arrays` - Various array formats
- `arrays_of_arrays` - Nested arrays
- `objects` - Object encoding
- `mixed_arrays` - Mixed-type arrays
- `delimiters` - Custom delimiters
- `round_trip` - Encode/decode round-trips
- `decode_strict` - Strict validation
- `empty_and_root` - Edge cases

---

## Resources

- 📖 [TOON Specification v2.0](https://github.com/toon-format/spec/blob/main/SPEC.md)
- 📖 [TOON Specification v3.0](https://github.com/toon-format/spec/blob/main/SPEC.md)
- 📦 [Crates.io Package](https://crates.io/crates/toon-format)
- 📚 [API Documentation](https://docs.rs/toon-format)
- 🔧 [Main Repository (JS/TS)](https://github.com/toon-format/toon)
Expand Down Expand Up @@ -577,6 +673,12 @@ cargo fmt

# Build docs
cargo doc --open

# Fuzz targets (requires nightly + cargo-fuzz)
cargo install cargo-fuzz
cargo +nightly fuzz build
cargo +nightly fuzz run fuzz_decode -- -max_total_time=10
cargo +nightly fuzz run fuzz_encode -- -max_total_time=10
```

---
Expand Down
92 changes: 92 additions & 0 deletions benches/encode_decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use serde_json::{json, Value};
use toon_format::{decode_default, encode_default};

fn make_tabular(rows: usize) -> Value {
let mut items = Vec::with_capacity(rows);
for i in 0..rows {
items.push(json!({
"id": i,
"name": format!("User_{i}"),
"score": i * 2,
"active": i % 2 == 0,
"tag": format!("tag{i}"),
}));
}
Value::Array(items)
}

fn make_deep_object(depth: usize) -> Value {
let mut value = json!({
"leaf": "value",
"count": 1,
});

for i in 0..depth {
value = json!({
format!("level_{i}"): value,
});
}

value
}

fn make_long_unquoted(words: usize) -> String {
let mut parts = Vec::with_capacity(words);
for i in 0..words {
parts.push(format!("word{i}"));
}
parts.join(" ")
}

fn bench_tabular(c: &mut Criterion) {
let mut group = c.benchmark_group("tabular");
for rows in [128_usize, 1024] {
let value = make_tabular(rows);
let toon = encode_default(&value).expect("encode tabular");

group.bench_with_input(BenchmarkId::new("encode", rows), &value, |b, val| {
b.iter(|| encode_default(black_box(val)).expect("encode tabular"));
});

group.bench_with_input(BenchmarkId::new("decode", rows), &toon, |b, input| {
b.iter(|| decode_default::<Value>(black_box(input)).expect("decode tabular"));
});
}
group.finish();
}

fn bench_deep_object(c: &mut Criterion) {
let mut group = c.benchmark_group("deep_object");
for depth in [32_usize, 128] {
let value = make_deep_object(depth);
let toon = encode_default(&value).expect("encode deep object");

group.bench_with_input(BenchmarkId::new("encode", depth), &value, |b, val| {
b.iter(|| encode_default(black_box(val)).expect("encode deep object"));
});

group.bench_with_input(BenchmarkId::new("decode", depth), &toon, |b, input| {
b.iter(|| decode_default::<Value>(black_box(input)).expect("decode deep object"));
});
}
group.finish();
}

fn bench_long_unquoted(c: &mut Criterion) {
let words = 512;
let long_value = make_long_unquoted(words);
let toon = format!("value: {long_value}");

c.bench_function("decode_long_unquoted", |b| {
b.iter(|| decode_default::<Value>(black_box(&toon)).expect("decode long unquoted"));
});
}

criterion_group!(
benches,
bench_tabular,
bench_deep_object,
bench_long_unquoted
);
criterion_main!(benches);
Loading
Loading