Skip to content

Commit f151e8c

Browse files
Merge pull request mischasigtermans#12 from mischasigtermans/feat/spec-test-fixtures
TOON v3.0 Spec Compliance
2 parents edb1e91 + 3eef0bc commit f151e8c

39 files changed

+1942
-251
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,8 @@ jobs:
5252
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
5353
composer update --prefer-stable --prefer-dist --no-interaction
5454
55+
- name: Install spec fixtures
56+
run: npm install
57+
5558
- name: Execute tests
5659
run: vendor/bin/pest

CHANGELOG.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ All notable changes to this project will be documented in this file.
77
### Added
88
- Full [TOON v3.0 specification](https://github.com/toon-format/spec/blob/main/SPEC.md) compliance
99
- Global helper functions: `toon_encode()` and `toon_decode()`
10-
- Performance optimizations for encoding and decoding
11-
- Config injection support for encoder/decoder constructors (improved testability)
1210
- Spec-compliant string quoting (strings with special characters are now quoted with `"..."`)
1311
- Proper escape sequences within quoted strings (`\n`, `\r`, `\t`, `\"`, `\\`)
1412
- Delimiter support: comma (default), tab (`\t`), and pipe (`|`) via `delimiter` config option
1513
- Strict mode for decoding with validation errors via `strict` config option
16-
- Official specification test suite (65 compliance tests)
14+
- Key folding: collapse single-key nested objects into dot notation via `key_folding` config
15+
- Path expansion: expand dotted keys back to nested objects via `expand_paths` config
16+
- Official specification test fixtures from [toon-format/spec](https://github.com/toon-format/spec)
1717
- Inline primitive array format (`key[N]: a,b,c`)
1818

1919
### Changed
@@ -24,11 +24,12 @@ All notable changes to this project will be documented in this file.
2424
- Float encoding now preserves full IEEE 754 double precision (16 significant digits)
2525

2626
### Migration Guide
27-
The decoder maintains backward compatibility and will correctly parse both the old backslash-escaped format and the new quoted string format. However, if you have code that expects the old output format, be aware that:
28-
1. Encoded output will now use quoted strings for special characters
29-
2. The `escape_style` config option has been removed
30-
3. Republish config to get new options: `php artisan vendor:publish --tag=toon-config --force`
31-
4. Set `strict => false` in config if parsing legacy TOON that may have formatting issues
27+
The decoder maintains backward compatibility and parses both old backslash-escaped format and new quoted strings.
28+
29+
If you have code that expects the old output format:
30+
1. Encoded output now uses quoted strings for special characters
31+
2. Republish config: `php artisan vendor:publish --tag=toon-config --force`
32+
3. Set `strict => false` if parsing legacy TOON with formatting issues
3233

3334
## [0.2.2] - 2025-12-28
3435

@@ -63,11 +64,10 @@ The decoder maintains backward compatibility and will correctly parse both the o
6364

6465
### Added
6566
- Initial release
66-
- TOON encoding with automatic nested object flattening using dot notation
67+
- TOON encoding with tabular array format
6768
- TOON decoding with nested object reconstruction
68-
- Tabular array format for compact representation
6969
- Type preservation (int, float, bool, null)
7070
- Special character escaping (comma, colon, newline)
71-
- Configurable flatten depth and table thresholds
71+
- Configurable table thresholds
7272
- Laravel 9, 10, 11, and 12 support
7373
- PHP 8.2+ support

README.md

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
[![Latest Version on Packagist](https://img.shields.io/packagist/v/mischasigtermans/laravel-toon.svg?style=flat-square)](https://packagist.org/packages/mischasigtermans/laravel-toon)
44
[![Total Downloads](https://img.shields.io/packagist/dt/mischasigtermans/laravel-toon.svg?style=flat-square)](https://packagist.org/packages/mischasigtermans/laravel-toon)
55

6-
A spec-compliant TOON (Token-Optimized Object Notation) encoder/decoder for Laravel with intelligent nested object handling.
6+
The most complete [TOON](https://toonformat.dev/) implementation for Laravel, and the only one with full [TOON v3.0 specification](https://github.com/toon-format/spec/blob/main/SPEC.md) compliance.
77

8-
TOON is a compact, YAML-like format designed to reduce token usage when sending data to LLMs. This package implements the [TOON v3.0 specification](https://github.com/toon-format/spec/blob/main/SPEC.md) and achieves **40-60% token reduction** compared to JSON while maintaining full round-trip fidelity.
8+
TOON (Token-Optimized Object Notation) is a compact, YAML-like format designed to reduce token usage when sending data to LLMs. This package achieves **~50% token reduction** compared to JSON while maintaining full round-trip fidelity, backed by 470 tests.
99

1010
## Installation
1111

@@ -20,8 +20,8 @@ use MischaSigtermans\Toon\Facades\Toon;
2020

2121
$data = [
2222
'users' => [
23-
['id' => 1, 'name' => 'Alice', 'role' => ['id' => 'admin', 'level' => 10]],
24-
['id' => 2, 'name' => 'Bob', 'role' => ['id' => 'user', 'level' => 1]],
23+
['id' => 1, 'name' => 'Alice', 'active' => true],
24+
['id' => 2, 'name' => 'Bob', 'active' => false],
2525
],
2626
];
2727

@@ -34,10 +34,9 @@ $original = Toon::decode($toon);
3434

3535
**Output:**
3636
```
37-
users:
38-
items[2]{id,name,role.id,role.level}:
39-
1,Alice,admin,10
40-
2,Bob,user,1
37+
users[2]{id,name,active}:
38+
1,Alice,true
39+
2,Bob,false
4140
```
4241

4342
### Global Helper Functions
@@ -84,78 +83,76 @@ $toon = $user->toToon();
8483

8584
When building MCP servers or LLM-powered applications, every token counts. JSON's verbosity wastes context window space with repeated keys and structural characters.
8685

87-
**JSON (398 bytes):**
86+
**JSON (201 bytes):**
8887
```json
89-
{"orders":[{"id":"ord_1","status":"shipped","customer":{"id":"cust_1","name":"Alice"},"total":99.99},{"id":"ord_2","status":"pending","customer":{"id":"cust_2","name":"Bob"},"total":149.50}]}
88+
{"users":[{"id":1,"name":"Alice","role":"admin"},{"id":2,"name":"Bob","role":"user"},{"id":3,"name":"Carol","role":"user"}]}
9089
```
9190

92-
**TOON (186 bytes) - 53% smaller:**
91+
**TOON (62 bytes) - 69% smaller:**
9392
```
94-
orders:
95-
items[2]{id,status,customer.id,customer.name,total}:
96-
ord_1,shipped,cust_1,Alice,99.99
97-
ord_2,pending,cust_2,Bob,149.5
93+
users[3]{id,name,role}:
94+
1,Alice,admin
95+
2,Bob,user
96+
3,Carol,user
9897
```
9998

10099
## Benchmarks
101100

101+
For a typical paginated API response (50 records):
102+
- **JSON**: ~7,597 tokens
103+
- **TOON**: ~3,586 tokens
104+
- **Saved**: ~4,000 tokens per request
105+
102106
Real-world benchmarks from a production application with 17,000+ records:
103107

104108
| Data Type | JSON | TOON | Savings |
105109
|-----------|------|------|---------|
106-
| 50 records (nested objects) | 13,055 bytes | 5,080 bytes | **61%** |
107-
| 100 records (nested objects) | 26,156 bytes | 10,185 bytes | **61%** |
108-
| 500 records (nested objects) | 129,662 bytes | 49,561 bytes | **62%** |
109-
| 1,000 records (nested objects) | 258,965 bytes | 98,629 bytes | **62%** |
110-
| 100 records (mixed nesting) | 43,842 bytes | 26,267 bytes | **40%** |
111-
| Single object | 169 bytes | 124 bytes | **27%** |
112-
113-
### Token Impact
114-
115-
For a typical paginated API response (50 records):
116-
- **JSON**: ~3,274 tokens
117-
- **TOON**: ~1,279 tokens
118-
- **Saved**: ~2,000 tokens per request
110+
| 50 records | 30,389 bytes | 14,343 bytes | **53%** |
111+
| 100 records | 60,856 bytes | 28,498 bytes | **53%** |
112+
| 500 records | 303,549 bytes | 140,154 bytes | **54%** |
113+
| 1,000 records | 604,408 bytes | 277,614 bytes | **54%** |
119114

120115
## Features
121116

122-
### Nested Object Flattening
117+
### Tabular Format
123118

124-
The key differentiator. Arrays containing objects with nested properties are automatically flattened using dot notation:
119+
Arrays of uniform objects with primitive values are encoded as compact tables:
125120

126121
```php
127122
$data = [
128-
['id' => 1, 'author' => ['name' => 'Jane', 'email' => 'jane@example.com']],
129-
['id' => 2, 'author' => ['name' => 'John', 'email' => 'john@example.com']],
123+
['id' => 1, 'name' => 'Alice', 'role' => 'admin'],
124+
['id' => 2, 'name' => 'Bob', 'role' => 'user'],
130125
];
131126

132127
$toon = Toon::encode($data);
133-
// items[2]{id,author.name,author.email}:
134-
// 1,Jane,jane@example.com
135-
// 2,John,john@example.com
136-
137-
$decoded = Toon::decode($toon);
138-
// Returns original nested structure
128+
// [2]{id,name,role}:
129+
// 1,Alice,admin
130+
// 2,Bob,user
139131
```
140132

141-
### Multi-Level Nesting
133+
### List Format for Nested Objects
142134

143-
Handles deeply nested structures:
135+
Arrays containing objects with nested properties use list format for clarity:
144136

145137
```php
146138
$data = [
147-
[
148-
'id' => 1,
149-
'product' => [
150-
'name' => 'Widget',
151-
'category' => ['id' => 'cat_1', 'name' => 'Electronics'],
152-
],
153-
],
139+
['id' => 1, 'author' => ['name' => 'Jane', 'email' => 'jane@example.com']],
140+
['id' => 2, 'author' => ['name' => 'John', 'email' => 'john@example.com']],
154141
];
155142

156143
$toon = Toon::encode($data);
157-
// items[1]{id,product.name,product.category.id,product.category.name}:
158-
// 1,Widget,cat_1,Electronics
144+
// [2]:
145+
// - id: 1
146+
// author:
147+
// name: Jane
148+
// email: jane@example.com
149+
// - id: 2
150+
// author:
151+
// name: John
152+
// email: john@example.com
153+
154+
$decoded = Toon::decode($toon);
155+
// Returns original nested structure
159156
```
160157

161158
### Type Preservation
@@ -207,9 +204,6 @@ return [
207204
// Arrays with fewer items use regular object format instead of tables
208205
'min_rows_for_table' => 2,
209206

210-
// How deep to flatten nested objects (deeper = JSON string)
211-
'max_flatten_depth' => 3,
212-
213207
// Delimiter for array values: ',' (default), '\t' (tab), or '|' (pipe)
214208
'delimiter' => ',',
215209

@@ -335,18 +329,19 @@ This package implements the [TOON v3.0 specification](https://github.com/toon-fo
335329

336330
- **String quoting**: Safe strings unquoted, special characters properly escaped (`\n`, `\r`, `\t`, `\"`, `\\`)
337331
- **Delimiter support**: Comma (default), tab, and pipe delimiters
338-
- **Array formats**: Inline primitives (`[N]: a,b,c`) and tabular objects (`[N]{fields}:`)
339-
- **Nested object flattening**: Dot notation for nested properties in tabular format
332+
- **Tabular format**: Compact tables for arrays of primitive-only objects (`[N]{fields}:`)
333+
- **List format**: Readable structure for arrays with nested objects (`[N]:` with `- field:` items)
334+
- **Inline arrays**: Primitive arrays on single line (`key[N]: a,b,c`)
340335
- **Strict mode**: Optional validation during decoding
341-
- **Backward compatibility**: Decoder accepts legacy backslash escaping
336+
- **Backward compatibility**: Decoder accepts legacy formats (backslash escaping, dot-notation columns)
342337

343338
## Testing
344339

345340
```bash
346341
composer test
347342
```
348343

349-
The test suite includes 118 tests covering encoding, decoding, nested object handling, and official spec compliance fixtures.
344+
The test suite includes 470 tests covering encoding, decoding, nested object handling, and official spec compliance fixtures.
350345

351346
## Requirements
352347

config/toon.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,42 @@
6767
*/
6868
'strict' => true,
6969

70+
/*
71+
|--------------------------------------------------------------------------
72+
| Key Folding
73+
|--------------------------------------------------------------------------
74+
|
75+
| When set to 'safe', single-key nested objects are folded into dot notation.
76+
| Example: {user: {name: "Alice"}} becomes user.name: Alice
77+
|
78+
| Supported values: 'off' (default), 'safe'
79+
|
80+
*/
81+
'key_folding' => 'off',
82+
83+
/*
84+
|--------------------------------------------------------------------------
85+
| Key Folding Depth
86+
|--------------------------------------------------------------------------
87+
|
88+
| Maximum depth for key folding. Only applies when key_folding is 'safe'.
89+
|
90+
*/
91+
'key_folding_depth' => null,
92+
93+
/*
94+
|--------------------------------------------------------------------------
95+
| Expand Paths
96+
|--------------------------------------------------------------------------
97+
|
98+
| When set to 'safe', dotted keys are expanded back to nested objects
99+
| during decoding. Example: user.name: Alice becomes {user: {name: "Alice"}}
100+
|
101+
| Supported values: 'off' (default), 'safe'
102+
|
103+
*/
104+
'expand_paths' => 'off',
105+
70106
/*
71107
|--------------------------------------------------------------------------
72108
| Omit Values

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"devDependencies": {
3+
"@toon-format/spec": "^3.0.1"
4+
}
5+
}

phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
<testsuite name="Feature">
77
<directory suffix="Test.php">./tests/Feature</directory>
88
</testsuite>
9+
<testsuite name="Specs">
10+
<directory suffix="Test.php">./tests/Specs</directory>
11+
</testsuite>
912
</testsuites>
1013
</phpunit>

0 commit comments

Comments
 (0)