Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ users[2]{id,name}:
- **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 @@ -577,6 +644,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);
5 changes: 1 addition & 4 deletions examples/parts/arrays.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use serde::{
Deserialize,
Serialize,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use toon_format::encode_default;

Expand Down
5 changes: 1 addition & 4 deletions examples/parts/arrays_of_arrays.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use serde::{
Deserialize,
Serialize,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use toon_format::encode_default;

Expand Down
5 changes: 1 addition & 4 deletions examples/parts/decode_strict.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use serde_json::Value;
use toon_format::{
decode,
DecodeOptions,
};
use toon_format::{decode, DecodeOptions};

pub fn decode_strict() {
// Malformed: header says 2 rows, but only 1 provided
Expand Down
6 changes: 1 addition & 5 deletions examples/parts/delimiters.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
use serde_json::json;
use toon_format::{
encode,
Delimiter,
EncodeOptions,
};
use toon_format::{encode, Delimiter, EncodeOptions};

pub fn delimiters() {
let data = json!({
Expand Down
5 changes: 1 addition & 4 deletions examples/parts/mixed_arrays.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use serde::{
Deserialize,
Serialize,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use toon_format::encode_default;

Expand Down
5 changes: 1 addition & 4 deletions examples/parts/objects.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use serde::{
Deserialize,
Serialize,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use toon_format::encode_default;

Expand Down
15 changes: 3 additions & 12 deletions examples/parts/round_trip.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
use serde::{
Deserialize,
Serialize,
};
use serde_json::{
json,
Value,
};
use toon_format::{
decode_default,
encode_default,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use toon_format::{decode_default, encode_default};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Product {
Expand Down
10 changes: 2 additions & 8 deletions examples/parts/structs.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
use serde::{
Deserialize,
Serialize,
};
use toon_format::{
decode_default,
encode_default,
};
use serde::{Deserialize, Serialize};
use toon_format::{decode_default, encode_default};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct User {
Expand Down
5 changes: 1 addition & 4 deletions examples/parts/tabular.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use serde::{
Deserialize,
Serialize,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use toon_format::encode_default;

Expand Down
28 changes: 28 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "toon-format-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
toon-format = { path = ".." }
arbitrary = { version = "1", features = ["derive"] }
serde_json = "1"

[[bin]]
name = "fuzz_decode"
path = "fuzz_targets/decode.rs"
test = false
doc = false
bench = false

[[bin]]
name = "fuzz_encode"
path = "fuzz_targets/encode.rs"
test = false
doc = false
bench = false
Loading
Loading