toon_my_json 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE.txt +21 -0
- data/README.md +329 -0
- data/Rakefile +9 -0
- data/bin/toon +79 -0
- data/lib/toon_my_json/decoder.rb +286 -0
- data/lib/toon_my_json/encoder.rb +183 -0
- data/lib/toon_my_json/version.rb +5 -0
- data/lib/toon_my_json.rb +53 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9e4ebddf5d14d6d65b6205dd7bd42d14e1010e83c4312f5dbf4a6fd913c2012b
|
|
4
|
+
data.tar.gz: e4c5e7d10c381d40f73cfb6d7883b72758cf39bb2ac6c989726c71a2c92b673d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ffbeb4e4fcd13b5ceb76dda9718bbaf4e1cf7d9ad7fcaa1a31fc1ab4b8a36827aab7f1f80995966b84458bdc41e3248f7e64c738e074e1e12cf2a3bb9c455338
|
|
7
|
+
data.tar.gz: 8c94d7b7006f63204ec1eef550528bbd66fecad2ba3e1fea530d0aaa5be84af1f63866942e081f1e00080fac7000250ac377ade6615036a63290b2d34b6e9e53
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-11-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of toon_my_json gem
|
|
12
|
+
- `ToonMyJson.encode` - Convert JSON/Ruby objects to TOON format
|
|
13
|
+
- `ToonMyJson.decode` - Convert TOON format back to JSON/Ruby objects
|
|
14
|
+
- Bidirectional conversion support (JSON ↔ TOON)
|
|
15
|
+
- Tabular format for uniform arrays (30-60% space savings)
|
|
16
|
+
- Smart string quoting (only when necessary)
|
|
17
|
+
- Support for nested structures (objects and arrays)
|
|
18
|
+
- Lossless roundtrip conversions
|
|
19
|
+
- Command-line interface (`toon` command)
|
|
20
|
+
- `--encode` flag for JSON to TOON conversion (default)
|
|
21
|
+
- `--decode` flag for TOON to JSON conversion
|
|
22
|
+
- `--indent` option for custom indentation
|
|
23
|
+
- `--delimiter` option for custom field delimiters
|
|
24
|
+
- `--no-length-marker` option to disable array length markers
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
- Automatic detection of uniform arrays for tabular formatting
|
|
28
|
+
- Handles primitives, objects, arrays, and nested structures
|
|
29
|
+
- Multiple input types: JSON strings, Ruby objects, TOON strings
|
|
30
|
+
- Customizable encoding options (indent, delimiter, length markers)
|
|
31
|
+
- Ruby API and CLI support
|
|
32
|
+
|
|
33
|
+
[0.1.0]: https://github.com/mykyta/toon-my-json/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mykyta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# ToonMyJson
|
|
2
|
+
|
|
3
|
+
A Ruby gem for bidirectional conversion between JSON and TOON (Token-Oriented Object Notation) format. TOON is a compact serialization format designed for Large Language Models that reduces token usage by 30-60% compared to JSON.
|
|
4
|
+
|
|
5
|
+
## What is TOON?
|
|
6
|
+
|
|
7
|
+
TOON is a compact, human-readable format that combines the best of YAML's indentation-based structure with CSV's tabular format for arrays. It minimizes syntax overhead by removing redundant punctuation like braces, brackets, and unnecessary quotes.
|
|
8
|
+
|
|
9
|
+
### Format Comparison
|
|
10
|
+
|
|
11
|
+
**JSON (verbose):**
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"users": [
|
|
15
|
+
{ "id": 1, "name": "Alice", "role": "admin" },
|
|
16
|
+
{ "id": 2, "name": "Bob", "role": "user" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**TOON (compact):**
|
|
22
|
+
```
|
|
23
|
+
users:
|
|
24
|
+
[2]{id,name,role}:
|
|
25
|
+
1,Alice,admin
|
|
26
|
+
2,Bob,user
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Add this line to your application's Gemfile:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem 'toon_my_json'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And then execute:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install it yourself as:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
gem install toon_my_json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Ruby >= 3.0.0
|
|
52
|
+
- JSON gem (~> 2.0)
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require 'toon_my_json'
|
|
58
|
+
|
|
59
|
+
# Encode JSON to TOON
|
|
60
|
+
data = { "users" => [{ "id" => 1, "name" => "Alice" }] }
|
|
61
|
+
toon = ToonMyJson.encode(data)
|
|
62
|
+
# => "users:\n[1]{id,name}:\n 1,Alice"
|
|
63
|
+
|
|
64
|
+
# Decode TOON back to JSON
|
|
65
|
+
restored = ToonMyJson.decode(toon)
|
|
66
|
+
# => {"users"=>[{"id"=>1, "name"=>"Alice"}]}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### Ruby API
|
|
72
|
+
|
|
73
|
+
#### Encoding (JSON → TOON)
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
require 'toon_my_json'
|
|
77
|
+
|
|
78
|
+
# Convert a Ruby hash to TOON
|
|
79
|
+
data = { "name" => "Alice", "age" => 30 }
|
|
80
|
+
ToonMyJson.encode(data)
|
|
81
|
+
# => "name: Alice\nage: 30"
|
|
82
|
+
|
|
83
|
+
# Convert a JSON string to TOON
|
|
84
|
+
json = '{"name":"Alice","age":30}'
|
|
85
|
+
ToonMyJson.encode(json)
|
|
86
|
+
# => "name: Alice\nage: 30"
|
|
87
|
+
|
|
88
|
+
# Arrays automatically use tabular format for uniform data
|
|
89
|
+
data = [
|
|
90
|
+
{ "id" => 1, "name" => "Alice", "role" => "admin" },
|
|
91
|
+
{ "id" => 2, "name" => "Bob", "role" => "user" }
|
|
92
|
+
]
|
|
93
|
+
ToonMyJson.encode(data)
|
|
94
|
+
# => "[2]{id,name,role}:\n1,Alice,admin\n2,Bob,user"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Decoding (TOON → JSON)
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Convert TOON back to Ruby objects
|
|
101
|
+
toon = "name: Alice\nage: 30"
|
|
102
|
+
ToonMyJson.decode(toon)
|
|
103
|
+
# => {"name"=>"Alice", "age"=>30}
|
|
104
|
+
|
|
105
|
+
# Get JSON string output instead of Ruby object
|
|
106
|
+
ToonMyJson.decode(toon, json: true)
|
|
107
|
+
# => "{\n \"name\": \"Alice\",\n \"age\": 30\n}"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Roundtrip Conversion
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Perfect lossless conversion
|
|
114
|
+
original = { "company" => "TechCorp", "year" => 2020 }
|
|
115
|
+
toon = ToonMyJson.encode(original)
|
|
116
|
+
restored = ToonMyJson.decode(toon)
|
|
117
|
+
# => {"company"=>"TechCorp", "year"=>2020}
|
|
118
|
+
original == restored # => true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Configuration Options
|
|
122
|
+
|
|
123
|
+
#### Encoding Options
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Custom indentation (default: 2)
|
|
127
|
+
ToonMyJson.encode(data, indent: 4)
|
|
128
|
+
|
|
129
|
+
# Custom delimiter for arrays (default: ',')
|
|
130
|
+
ToonMyJson.encode(data, delimiter: '|')
|
|
131
|
+
|
|
132
|
+
# Disable length markers (default: true)
|
|
133
|
+
ToonMyJson.encode(data, length_marker: false)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Decoding Options
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Custom indentation for JSON output (default: 2)
|
|
140
|
+
ToonMyJson.decode(toon, indent: 4)
|
|
141
|
+
|
|
142
|
+
# Custom delimiter (must match what was used in encoding)
|
|
143
|
+
ToonMyJson.decode(toon, delimiter: '|')
|
|
144
|
+
|
|
145
|
+
# Get JSON string output instead of Ruby object
|
|
146
|
+
ToonMyJson.decode(toon, json: true)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Command Line Interface
|
|
150
|
+
|
|
151
|
+
The gem includes a `toon` CLI tool for converting between JSON and TOON formats:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Encode JSON to TOON (default)
|
|
155
|
+
$ toon input.json
|
|
156
|
+
$ echo '{"name":"Alice","age":30}' | toon
|
|
157
|
+
|
|
158
|
+
# Decode TOON to JSON
|
|
159
|
+
$ toon --decode input.toon
|
|
160
|
+
$ echo -e 'name: Alice\nage: 30' | toon --decode
|
|
161
|
+
|
|
162
|
+
# Roundtrip conversion
|
|
163
|
+
$ echo '{"name":"Alice"}' | toon | toon --decode
|
|
164
|
+
|
|
165
|
+
# Options
|
|
166
|
+
$ toon --indent 4 --delimiter '|' input.json # Custom formatting
|
|
167
|
+
$ toon --no-length-marker input.json # Disable array length markers
|
|
168
|
+
$ toon --decode --delimiter '|' input.toon # Decode with custom delimiter
|
|
169
|
+
|
|
170
|
+
# Help and version
|
|
171
|
+
$ toon --help
|
|
172
|
+
$ toon --version
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Features
|
|
176
|
+
|
|
177
|
+
- **Bidirectional Conversion**: Encode JSON to TOON and decode TOON back to JSON
|
|
178
|
+
- **Tabular Format**: Automatically detects uniform arrays of objects and converts them to compact tabular format
|
|
179
|
+
- **Smart Quoting**: Only adds quotes when necessary (special characters, reserved words, etc.)
|
|
180
|
+
- **Nested Structures**: Handles deeply nested objects and arrays
|
|
181
|
+
- **Lossless Roundtrips**: Encode and decode without data loss
|
|
182
|
+
- **Flexible Options**: Customize indentation, delimiters, and length markers
|
|
183
|
+
- **CLI Tool**: Convert files from the command line with full encode/decode support
|
|
184
|
+
- **Multiple Input Types**: Accepts JSON strings, Ruby objects, or TOON strings
|
|
185
|
+
|
|
186
|
+
## Advanced Examples
|
|
187
|
+
|
|
188
|
+
### Complex Nested Structure
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
data = {
|
|
192
|
+
"company" => "TechCorp",
|
|
193
|
+
"employees" => [
|
|
194
|
+
{ "id" => 1, "name" => "Alice", "department" => "Engineering" },
|
|
195
|
+
{ "id" => 2, "name" => "Bob", "department" => "Sales" }
|
|
196
|
+
],
|
|
197
|
+
"metadata" => {
|
|
198
|
+
"founded" => 2020,
|
|
199
|
+
"location" => "San Francisco"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
puts ToonMyJson.encode(data)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Output:**
|
|
207
|
+
```
|
|
208
|
+
company: TechCorp
|
|
209
|
+
employees:
|
|
210
|
+
[2]{id,name,department}:
|
|
211
|
+
1,Alice,Engineering
|
|
212
|
+
2,Bob,Sales
|
|
213
|
+
metadata:
|
|
214
|
+
founded: 2020
|
|
215
|
+
location: San Francisco
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Primitive Arrays
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
data = { "colors" => ["red", "green", "blue"] }
|
|
222
|
+
ToonMyJson.encode(data)
|
|
223
|
+
# => "colors: red,green,blue"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Mixed Arrays
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
data = ["string", 42, { "key" => "value" }]
|
|
230
|
+
ToonMyJson.encode(data)
|
|
231
|
+
# => "- string\n- 42\n- key: value"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Decoding Examples
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Decode simple hash
|
|
238
|
+
toon = <<~TOON
|
|
239
|
+
name: Alice
|
|
240
|
+
age: 30
|
|
241
|
+
TOON
|
|
242
|
+
ToonMyJson.decode(toon)
|
|
243
|
+
# => {"name"=>"Alice", "age"=>30}
|
|
244
|
+
|
|
245
|
+
# Decode tabular array
|
|
246
|
+
toon = <<~TOON
|
|
247
|
+
[2]{id,name,role}:
|
|
248
|
+
1,Alice,admin
|
|
249
|
+
2,Bob,user
|
|
250
|
+
TOON
|
|
251
|
+
ToonMyJson.decode(toon)
|
|
252
|
+
# => [{"id"=>1, "name"=>"Alice", "role"=>"admin"}, {"id"=>2, "name"=>"Bob", "role"=>"user"}]
|
|
253
|
+
|
|
254
|
+
# Decode complex nested structure
|
|
255
|
+
toon = <<~TOON
|
|
256
|
+
company: TechCorp
|
|
257
|
+
employees:
|
|
258
|
+
[2]{id,name,department}:
|
|
259
|
+
1,Alice,Engineering
|
|
260
|
+
2,Bob,Sales
|
|
261
|
+
metadata:
|
|
262
|
+
founded: 2020
|
|
263
|
+
location: San Francisco
|
|
264
|
+
TOON
|
|
265
|
+
result = ToonMyJson.decode(toon)
|
|
266
|
+
# => {"company"=>"TechCorp", "employees"=>[...], "metadata"=>{...}}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
After checking out the repo, run `bundle install` to install dependencies:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
bundle install
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Run the test suite:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
bundle exec rspec
|
|
281
|
+
# or
|
|
282
|
+
bundle exec rake spec
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This represents complete test coverage for production Ruby code, ensuring all code paths and conditional branches are thoroughly tested.
|
|
286
|
+
|
|
287
|
+
Run performance benchmarks (performance can be improved):
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Run all benchmark tests
|
|
291
|
+
bundle exec rspec --tag benchmark
|
|
292
|
+
|
|
293
|
+
# Or with environment variable
|
|
294
|
+
BENCHMARK=1 bundle exec rspec
|
|
295
|
+
|
|
296
|
+
# Run only the benchmark-ips comparison (shows iterations/second)
|
|
297
|
+
bundle exec rspec --tag ips
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Performance benchmarks validate:
|
|
301
|
+
- Encoding 1000 records completes in under 10ms
|
|
302
|
+
- Decoding 1000 records completes in under 50ms
|
|
303
|
+
- Roundtrip conversion completes in under 60ms
|
|
304
|
+
- Iterations per second for common operations
|
|
305
|
+
|
|
306
|
+
Install the gem locally:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
bundle exec rake install
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Build the gem:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
bundle exec rake build
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Contributing
|
|
319
|
+
|
|
320
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mykbren/toon-my-json.
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
325
|
+
|
|
326
|
+
## References
|
|
327
|
+
|
|
328
|
+
- [TOON Format Specification](https://github.com/toon-format/toon)
|
|
329
|
+
- [Original TypeScript Implementation](https://github.com/toon-format/toon)
|
data/Rakefile
ADDED
data/bin/toon
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/toon_my_json'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
|
|
7
|
+
options = {
|
|
8
|
+
indent: 2,
|
|
9
|
+
delimiter: ',',
|
|
10
|
+
length_marker: true,
|
|
11
|
+
mode: :encode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
OptionParser.new do |opts|
|
|
15
|
+
opts.banner = 'Usage: toon [options] [file]'
|
|
16
|
+
opts.separator ''
|
|
17
|
+
opts.separator 'Convert between JSON and TOON formats'
|
|
18
|
+
opts.separator ''
|
|
19
|
+
opts.separator 'Options:'
|
|
20
|
+
|
|
21
|
+
opts.on('-e', '--encode', 'Encode JSON to TOON (default)') do
|
|
22
|
+
options[:mode] = :encode
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on('-D', '--decode', 'Decode TOON to JSON') do
|
|
26
|
+
options[:mode] = :decode
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on('-i', '--indent N', Integer, 'Indentation spaces (default: 2)') do |n|
|
|
30
|
+
options[:indent] = n
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on('-d', '--delimiter CHAR', String, 'Field delimiter (default: ,)') do |d|
|
|
34
|
+
options[:delimiter] = d
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on('--no-length-marker', 'Disable array length markers (encode only)') do
|
|
38
|
+
options[:length_marker] = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on('-h', '--help', 'Show this help message') do
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on('-v', '--version', 'Show version') do
|
|
47
|
+
puts "toon_my_json version #{ToonMyJson::VERSION}"
|
|
48
|
+
exit
|
|
49
|
+
end
|
|
50
|
+
end.parse!
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
# Read from file or stdin
|
|
54
|
+
input = if ARGV.empty?
|
|
55
|
+
$stdin.read
|
|
56
|
+
else
|
|
57
|
+
File.read(ARGV[0])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert and output
|
|
61
|
+
mode = options.delete(:mode)
|
|
62
|
+
result = if mode == :decode
|
|
63
|
+
# Remove encode-only options
|
|
64
|
+
decode_options = options.slice(:indent, :delimiter)
|
|
65
|
+
ToonMyJson.decode(input, **decode_options, json: true)
|
|
66
|
+
else
|
|
67
|
+
ToonMyJson.encode(input, **options)
|
|
68
|
+
end
|
|
69
|
+
puts result
|
|
70
|
+
rescue JSON::ParserError => e
|
|
71
|
+
warn "Error: Invalid JSON - #{e.message}"
|
|
72
|
+
exit 1
|
|
73
|
+
rescue Errno::ENOENT => e
|
|
74
|
+
warn "Error: File not found - #{e.message}"
|
|
75
|
+
exit 1
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
warn "Error: #{e.message}"
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ToonMyJson
|
|
4
|
+
# Decodes TOON format back to Ruby objects
|
|
5
|
+
class Decoder
|
|
6
|
+
attr_reader :lines, :current_line, :delimiter
|
|
7
|
+
|
|
8
|
+
def initialize(indent: 2, delimiter: ',')
|
|
9
|
+
@indent_size = indent
|
|
10
|
+
@delimiter = delimiter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def decode(toon_string)
|
|
14
|
+
@lines = toon_string.split("\n")
|
|
15
|
+
@current_line = 0
|
|
16
|
+
|
|
17
|
+
# Detect if it's a single line
|
|
18
|
+
if @lines.length == 1
|
|
19
|
+
content = @lines[0].strip
|
|
20
|
+
|
|
21
|
+
# Check if it's a key-value (check this first!)
|
|
22
|
+
return parse_hash(0) if key_value_line?(content)
|
|
23
|
+
|
|
24
|
+
# Check if it's a primitive array (contains delimiter but not quotes around everything)
|
|
25
|
+
return parse_primitive_array(content) if content.include?(@delimiter) && !content.match(/^".*"$/)
|
|
26
|
+
|
|
27
|
+
# Otherwise it's a single primitive
|
|
28
|
+
return parse_primitive(content)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Multi-line parsing
|
|
32
|
+
parse_value(0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def parse_value(expected_indent)
|
|
38
|
+
return nil if @current_line >= @lines.length
|
|
39
|
+
|
|
40
|
+
line = @lines[@current_line]
|
|
41
|
+
indent = get_indent(line)
|
|
42
|
+
|
|
43
|
+
return nil if indent < expected_indent
|
|
44
|
+
|
|
45
|
+
content = line.strip
|
|
46
|
+
|
|
47
|
+
# Check for tabular array header [N]{fields}: or {fields}:
|
|
48
|
+
return parse_tabular_array(indent) if content.match(/^(?:\[\d+\])?\{[^}]+\}:$/)
|
|
49
|
+
|
|
50
|
+
# Check for list array (lines starting with -)
|
|
51
|
+
return parse_list_array(indent) if content.start_with?('-')
|
|
52
|
+
|
|
53
|
+
# Check if this looks like a hash (has key-value pairs)
|
|
54
|
+
return parse_hash(indent) if key_value_line?(content)
|
|
55
|
+
|
|
56
|
+
# Single primitive
|
|
57
|
+
@current_line += 1
|
|
58
|
+
parse_primitive(content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def key_value_line?(line)
|
|
62
|
+
# A key-value line has a colon, but the colon should not be inside quotes
|
|
63
|
+
# Use split_key_value to check and avoid duplicate logic
|
|
64
|
+
_, value = split_key_value(line)
|
|
65
|
+
!value.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_hash(expected_indent)
|
|
69
|
+
hash = {}
|
|
70
|
+
|
|
71
|
+
while @current_line < @lines.length
|
|
72
|
+
line = @lines[@current_line]
|
|
73
|
+
indent = get_indent(line)
|
|
74
|
+
|
|
75
|
+
break if indent < expected_indent
|
|
76
|
+
|
|
77
|
+
content = line.strip
|
|
78
|
+
break if content.empty?
|
|
79
|
+
|
|
80
|
+
# Check if it's a tabular array header (not a key-value pair)
|
|
81
|
+
break if content.match(/^(?:\[\d+\])?\{[^}]+\}:$/)
|
|
82
|
+
|
|
83
|
+
# Check if it's a list array item
|
|
84
|
+
break if content.start_with?('-')
|
|
85
|
+
|
|
86
|
+
# Parse key-value pair
|
|
87
|
+
key, value_part = split_key_value(content)
|
|
88
|
+
if value_part.nil?
|
|
89
|
+
# Not a valid key-value line (no unquoted colon), stop parsing hash
|
|
90
|
+
break
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
key = parse_string(key.strip)
|
|
94
|
+
|
|
95
|
+
if value_part.strip.empty?
|
|
96
|
+
# Value on next lines (nested)
|
|
97
|
+
@current_line += 1
|
|
98
|
+
|
|
99
|
+
# Check if next line is a tabular array header (can be at any indent)
|
|
100
|
+
if @current_line < @lines.length
|
|
101
|
+
next_line = @lines[@current_line].strip
|
|
102
|
+
if next_line.match(/^(?:\[\d+\])?\{[^}]+\}:$/)
|
|
103
|
+
# Parse tabular array regardless of indent
|
|
104
|
+
hash[key] = parse_tabular_array(get_indent(@lines[@current_line]))
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# For nested values, accept same indent or greater
|
|
110
|
+
hash[key] = parse_value(expected_indent)
|
|
111
|
+
else
|
|
112
|
+
# Value on same line
|
|
113
|
+
value_part = value_part.strip
|
|
114
|
+
@current_line += 1
|
|
115
|
+
|
|
116
|
+
hash[key] = case value_part
|
|
117
|
+
when '[]'
|
|
118
|
+
[]
|
|
119
|
+
when '{}'
|
|
120
|
+
{}
|
|
121
|
+
else
|
|
122
|
+
# Could be primitive, primitive array, or inline object
|
|
123
|
+
if value_part.include?(@delimiter) && !value_part.match(/^".*"$/)
|
|
124
|
+
# Primitive array
|
|
125
|
+
parse_primitive_array(value_part)
|
|
126
|
+
else
|
|
127
|
+
parse_primitive(value_part)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
hash
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def split_key_value(line)
|
|
137
|
+
# Split by first colon not in quotes
|
|
138
|
+
in_quotes = false
|
|
139
|
+
line.each_char.with_index do |char, i|
|
|
140
|
+
if char == '"' && (i.zero? || line[i - 1] != '\\')
|
|
141
|
+
in_quotes = !in_quotes
|
|
142
|
+
elsif char == ':' && !in_quotes
|
|
143
|
+
return [line[0...i], line[(i + 1)..]]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
[line, nil]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_tabular_array(expected_indent)
|
|
150
|
+
line = @lines[@current_line].strip
|
|
151
|
+
|
|
152
|
+
# Parse header: [N]{field1,field2,...}: or {field1,field2,...}:
|
|
153
|
+
match = line.match(/^(?:\[\d+\])?\{([^}]+)\}:$/)
|
|
154
|
+
return [] unless match
|
|
155
|
+
|
|
156
|
+
fields = match[1].split(@delimiter).map(&:strip)
|
|
157
|
+
@current_line += 1
|
|
158
|
+
|
|
159
|
+
array = []
|
|
160
|
+
while @current_line < @lines.length
|
|
161
|
+
line = @lines[@current_line]
|
|
162
|
+
indent = get_indent(line)
|
|
163
|
+
|
|
164
|
+
break if indent <= expected_indent
|
|
165
|
+
|
|
166
|
+
content = line.strip
|
|
167
|
+
break if content.empty?
|
|
168
|
+
|
|
169
|
+
# Stop if we hit a key-value line (next section)
|
|
170
|
+
break if key_value_line?(content) && !content.match(/^(?:\[\d+\])?\{[^}]+\}:$/)
|
|
171
|
+
|
|
172
|
+
# Parse row
|
|
173
|
+
values = parse_csv_line(content)
|
|
174
|
+
row = {}
|
|
175
|
+
fields.each_with_index do |field, i|
|
|
176
|
+
row[field] = values[i] if i < values.length
|
|
177
|
+
end
|
|
178
|
+
array << row
|
|
179
|
+
@current_line += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
array
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parse_list_array(expected_indent)
|
|
186
|
+
array = []
|
|
187
|
+
|
|
188
|
+
while @current_line < @lines.length
|
|
189
|
+
line = @lines[@current_line]
|
|
190
|
+
indent = get_indent(line)
|
|
191
|
+
|
|
192
|
+
break if indent < expected_indent
|
|
193
|
+
|
|
194
|
+
content = line.strip
|
|
195
|
+
break unless content.start_with?('-')
|
|
196
|
+
|
|
197
|
+
# Remove leading dash and space
|
|
198
|
+
item_content = content[1..].strip
|
|
199
|
+
|
|
200
|
+
@current_line += 1
|
|
201
|
+
array << if item_content.empty?
|
|
202
|
+
# Multi-line item (next line)
|
|
203
|
+
parse_value(expected_indent + @indent_size)
|
|
204
|
+
else
|
|
205
|
+
# Inline item
|
|
206
|
+
parse_primitive(item_content)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
array
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parse_primitive_array(content)
|
|
214
|
+
parse_csv_line(content)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_csv_line(line)
|
|
218
|
+
values = []
|
|
219
|
+
current = String.new # Pre-allocate mutable string
|
|
220
|
+
in_quotes = false
|
|
221
|
+
i = 0
|
|
222
|
+
|
|
223
|
+
while i < line.length
|
|
224
|
+
char = line[i]
|
|
225
|
+
|
|
226
|
+
if char == '"' && (i.zero? || line[i - 1] != '\\')
|
|
227
|
+
in_quotes = !in_quotes
|
|
228
|
+
current << char
|
|
229
|
+
elsif char == @delimiter && !in_quotes
|
|
230
|
+
values << parse_primitive(current.strip)
|
|
231
|
+
current.clear
|
|
232
|
+
else
|
|
233
|
+
current << char
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
i += 1
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
values << parse_primitive(current.strip) unless current.strip.empty?
|
|
240
|
+
values
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def parse_primitive(value)
|
|
244
|
+
value = value.strip
|
|
245
|
+
|
|
246
|
+
# Handle quoted strings
|
|
247
|
+
if value.start_with?('"') && value.end_with?('"') && value.length > 1
|
|
248
|
+
# Remove quotes and unescape in single pass
|
|
249
|
+
return unescape_string(value[1...-1])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Handle special values
|
|
253
|
+
case value
|
|
254
|
+
when 'null'
|
|
255
|
+
nil
|
|
256
|
+
when 'true'
|
|
257
|
+
true
|
|
258
|
+
when 'false'
|
|
259
|
+
false
|
|
260
|
+
when /^-?\d+$/
|
|
261
|
+
value.to_i
|
|
262
|
+
when /^-?\d+\.\d+$/
|
|
263
|
+
value.to_f
|
|
264
|
+
else
|
|
265
|
+
value
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def parse_string(value)
|
|
270
|
+
if value.start_with?('"') && value.end_with?('"') && value.length > 1
|
|
271
|
+
unescape_string(value[1...-1])
|
|
272
|
+
else
|
|
273
|
+
value
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def unescape_string(str)
|
|
278
|
+
# Unescape only the specific escape sequences we support: \\ and \"
|
|
279
|
+
str.gsub(/\\\\|\\"/) { |match| match == '\\\\' ? '\\' : '"' }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def get_indent(line)
|
|
283
|
+
line.match(/^(\s*)/)[1].length
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ToonMyJson
|
|
4
|
+
# Encodes Ruby objects to TOON format
|
|
5
|
+
class Encoder
|
|
6
|
+
RESERVED_CHARS = /[,:\[\]{}#\n\r\t]/
|
|
7
|
+
NEEDS_QUOTES = /\A\s|\s\z|#{RESERVED_CHARS}/
|
|
8
|
+
|
|
9
|
+
attr_reader :indent_size, :delimiter, :length_marker
|
|
10
|
+
|
|
11
|
+
def initialize(indent: 2, delimiter: ',', length_marker: true)
|
|
12
|
+
@indent_size = indent
|
|
13
|
+
@delimiter = delimiter
|
|
14
|
+
@length_marker = length_marker
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def encode(value, depth = 0)
|
|
18
|
+
case value
|
|
19
|
+
when Hash
|
|
20
|
+
encode_hash(value, depth)
|
|
21
|
+
when Array
|
|
22
|
+
encode_array(value, depth)
|
|
23
|
+
when nil
|
|
24
|
+
'null'
|
|
25
|
+
when true, false
|
|
26
|
+
value.to_s
|
|
27
|
+
when Numeric
|
|
28
|
+
value.to_s
|
|
29
|
+
when String
|
|
30
|
+
encode_string(value)
|
|
31
|
+
else
|
|
32
|
+
encode_string(value.to_s)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def encode_hash(hash, depth)
|
|
39
|
+
return '{}' if hash.empty?
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
hash.each do |key, value|
|
|
43
|
+
encoded_value = encode_value_for_hash(value, depth)
|
|
44
|
+
lines << "#{indent(depth)}#{encode_key(key)}:#{encoded_value}"
|
|
45
|
+
end
|
|
46
|
+
lines.join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def encode_value_for_hash(value, depth)
|
|
50
|
+
case value
|
|
51
|
+
when Hash
|
|
52
|
+
if value.empty?
|
|
53
|
+
' {}'
|
|
54
|
+
else
|
|
55
|
+
"\n#{encode_hash(value, depth + 1)}"
|
|
56
|
+
end
|
|
57
|
+
when Array
|
|
58
|
+
if value.empty?
|
|
59
|
+
' []'
|
|
60
|
+
elsif uniform_array?(value) && value.first.is_a?(Hash)
|
|
61
|
+
"\n#{encode_tabular_array(value, depth + 1)}"
|
|
62
|
+
elsif primitive_array?(value)
|
|
63
|
+
" #{encode_primitive_array(value)}"
|
|
64
|
+
else
|
|
65
|
+
"\n#{encode_list_array(value, depth + 1)}"
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
" #{encode(value, depth)}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def encode_array(array, depth)
|
|
73
|
+
return '[]' if array.empty?
|
|
74
|
+
|
|
75
|
+
if uniform_array?(array) && array.first.is_a?(Hash)
|
|
76
|
+
encode_tabular_array(array, depth)
|
|
77
|
+
elsif primitive_array?(array)
|
|
78
|
+
encode_primitive_array(array)
|
|
79
|
+
else
|
|
80
|
+
encode_list_array(array, depth)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def encode_tabular_array(array, depth)
|
|
85
|
+
return '[]' if array.empty?
|
|
86
|
+
|
|
87
|
+
# Get all unique keys across all objects
|
|
88
|
+
keys = array.flat_map(&:keys).uniq
|
|
89
|
+
|
|
90
|
+
# Build header
|
|
91
|
+
length_prefix = @length_marker ? "[#{array.length}]" : ''
|
|
92
|
+
header = "#{length_prefix}{#{keys.join(delimiter)}}"
|
|
93
|
+
|
|
94
|
+
# Build rows
|
|
95
|
+
rows = array.map do |item|
|
|
96
|
+
row_values = keys.map { |key| encode(item[key] || item[key.to_sym], depth) }
|
|
97
|
+
"#{indent(depth)}#{row_values.join(delimiter)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
"#{header}:\n#{rows.join("\n")}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def encode_primitive_array(array)
|
|
104
|
+
array.map { |v| encode(v, 0) }.join(delimiter)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def encode_list_array(array, depth)
|
|
108
|
+
lines = array.map do |item|
|
|
109
|
+
case item
|
|
110
|
+
when Hash, Array
|
|
111
|
+
encoded = encode(item, depth + 1)
|
|
112
|
+
# If multiline, indent the nested structure
|
|
113
|
+
if encoded.include?("\n")
|
|
114
|
+
"#{indent(depth)}-\n#{indent_multiline(encoded, depth + 1)}"
|
|
115
|
+
else
|
|
116
|
+
"#{indent(depth)}- #{encoded}"
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
"#{indent(depth)}- #{encode(item, depth)}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
lines.join("\n")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def encode_key(key)
|
|
126
|
+
key_str = key.to_s
|
|
127
|
+
# Keys generally don't need quotes unless they contain special chars
|
|
128
|
+
key_str.match?(NEEDS_QUOTES) ? encode_string(key_str) : key_str
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def encode_string(str)
|
|
132
|
+
return '""' if str.empty?
|
|
133
|
+
|
|
134
|
+
# Check if string needs quotes
|
|
135
|
+
if str.match?(NEEDS_QUOTES) || looks_like_number?(str) || looks_like_boolean?(str)
|
|
136
|
+
# Escape quotes and backslashes
|
|
137
|
+
escaped = str.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
138
|
+
"\"#{escaped}\""
|
|
139
|
+
else
|
|
140
|
+
str
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def uniform_array?(array)
|
|
145
|
+
return false if array.empty? || !array.first.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
# Check if all elements are hashes with similar structure
|
|
148
|
+
first_keys = array.first.keys.sort
|
|
149
|
+
min_overlap = (first_keys.length * 0.8).ceil
|
|
150
|
+
|
|
151
|
+
array.all? do |item|
|
|
152
|
+
next false unless item.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
# Count matching keys without sorting every time
|
|
155
|
+
overlap = 0
|
|
156
|
+
item_keys = item.keys
|
|
157
|
+
first_keys.each { |key| overlap += 1 if item_keys.include?(key) }
|
|
158
|
+
overlap >= min_overlap
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def primitive_array?(array)
|
|
163
|
+
array.all? { |v| v.is_a?(String) || v.is_a?(Numeric) || v == true || v == false || v.nil? }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def looks_like_number?(str)
|
|
167
|
+
str.match?(/\A-?\d+(\.\d+)?\z/)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def looks_like_boolean?(str)
|
|
171
|
+
%w[true false null].include?(str)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def indent(depth)
|
|
175
|
+
' ' * (depth * @indent_size)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def indent_multiline(text, depth)
|
|
179
|
+
indent_str = indent(depth)
|
|
180
|
+
text.gsub(/^/, indent_str)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/toon_my_json.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'toon_my_json/version'
|
|
4
|
+
require_relative 'toon_my_json/encoder'
|
|
5
|
+
require_relative 'toon_my_json/decoder'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
# ToonMyJson provides bidirectional conversion between JSON and TOON format.
|
|
9
|
+
# TOON (Token-Oriented Object Notation) is a compact serialization format
|
|
10
|
+
# designed for LLMs that reduces token usage by 30-60% compared to JSON.
|
|
11
|
+
module ToonMyJson
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Convert a Ruby object or JSON string to TOON format
|
|
15
|
+
#
|
|
16
|
+
# @param input [String, Hash, Array, Object] JSON string or Ruby object
|
|
17
|
+
# @param options [Hash] Encoding options
|
|
18
|
+
# @option options [Integer] :indent Number of spaces per indentation level (default: 2)
|
|
19
|
+
# @option options [String] :delimiter Field delimiter for arrays (',', '\t', or '|') (default: ',')
|
|
20
|
+
# @option options [Boolean] :length_marker Include array length markers (default: true)
|
|
21
|
+
# @return [String] TOON formatted string
|
|
22
|
+
def self.encode(input, **options)
|
|
23
|
+
data = if input.is_a?(String) && (input.start_with?('{', '[') || input.strip.start_with?('{', '['))
|
|
24
|
+
begin
|
|
25
|
+
JSON.parse(input)
|
|
26
|
+
rescue JSON::ParserError
|
|
27
|
+
input
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
input
|
|
31
|
+
end
|
|
32
|
+
Encoder.new(**options).encode(data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Alias for encode
|
|
36
|
+
def self.convert(input, **options)
|
|
37
|
+
encode(input, **options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Convert TOON format string to Ruby object
|
|
41
|
+
#
|
|
42
|
+
# @param toon_string [String] TOON formatted string
|
|
43
|
+
# @param options [Hash] Decoding options
|
|
44
|
+
# @option options [Integer] :indent Number of spaces per indentation level (default: 2)
|
|
45
|
+
# @option options [String] :delimiter Field delimiter for arrays (',', '\t', or '|') (default: ',')
|
|
46
|
+
# @option options [Boolean] :json Return as JSON string instead of Ruby object (default: false)
|
|
47
|
+
# @return [Hash, Array, Object, String] Ruby object or JSON string
|
|
48
|
+
def self.decode(toon_string, **options)
|
|
49
|
+
json_output = options.delete(:json)
|
|
50
|
+
result = Decoder.new(**options).decode(toon_string)
|
|
51
|
+
json_output ? JSON.pretty_generate(result) : result
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toon_my_json
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- mykbren
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: benchmark-ips
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: bundler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: simplecov
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.22'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.22'
|
|
97
|
+
description: A Ruby gem for converting between JSON and TOON format. TOON is a compact
|
|
98
|
+
serialization format designed for LLMs that reduces token usage by 30-60% compared
|
|
99
|
+
to JSON. Supports bidirectional conversion, tabular arrays, nested structures, and
|
|
100
|
+
lossless roundtrips.
|
|
101
|
+
email:
|
|
102
|
+
- myk.bren@gmail.com
|
|
103
|
+
executables:
|
|
104
|
+
- toon
|
|
105
|
+
extensions: []
|
|
106
|
+
extra_rdoc_files: []
|
|
107
|
+
files:
|
|
108
|
+
- CHANGELOG.md
|
|
109
|
+
- LICENSE.txt
|
|
110
|
+
- README.md
|
|
111
|
+
- Rakefile
|
|
112
|
+
- bin/toon
|
|
113
|
+
- lib/toon_my_json.rb
|
|
114
|
+
- lib/toon_my_json/decoder.rb
|
|
115
|
+
- lib/toon_my_json/encoder.rb
|
|
116
|
+
- lib/toon_my_json/version.rb
|
|
117
|
+
homepage: https://github.com/mykbren/toon-my-json
|
|
118
|
+
licenses:
|
|
119
|
+
- MIT
|
|
120
|
+
metadata:
|
|
121
|
+
homepage_uri: https://github.com/mykbren/toon-my-json
|
|
122
|
+
source_code_uri: https://github.com/mykbren/toon-my-json
|
|
123
|
+
changelog_uri: https://github.com/mykbren/toon-my-json/blob/main/CHANGELOG.md
|
|
124
|
+
rubygems_mfa_required: 'true'
|
|
125
|
+
post_install_message:
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: 3.0.0
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 3.4.6
|
|
141
|
+
signing_key:
|
|
142
|
+
specification_version: 4
|
|
143
|
+
summary: Bidirectional JSON - TOON (Token-Oriented Object Notation) converter
|
|
144
|
+
test_files: []
|