toonify 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/.rspec +2 -0
- data/.rubocop.yml +27 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +65 -0
- data/README.md +100 -0
- data/Rakefile +14 -0
- data/examples/gemini_token_comparison.rb +126 -0
- data/lib/toonify/decoder.rb +122 -0
- data/lib/toonify/encoder.rb +107 -0
- data/lib/toonify/version.rb +5 -0
- data/lib/toonify.rb +42 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a4c3e9bd14eb81d809fcd9592c7e3366201d2bde28b79e259a90139d8b1b6b66
|
|
4
|
+
data.tar.gz: 39950dcc538a1d65c6d2f8f88e2af0bbc3c1ae27ac2253e0a2b9923f2e33a22d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c08cf024eabedc48f44601c32731eaafc5f7b06dbd9fd85aca5284676ebafc5d111f2c6678b4d31c5cb3f5e4024bfa479a2b56ec47220f3abfa0539c4a74e850
|
|
7
|
+
data.tar.gz: f547cd457555adc6aaea171904cde17d6c06b54a140e07f20c5c66b72f1905b4496ca760f55697372bb5ab660b51131ebcd080dd141d02f166c7d1cd9ccf5413
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
|
|
5
|
+
Metrics/BlockLength:
|
|
6
|
+
Exclude:
|
|
7
|
+
- 'spec/**/*'
|
|
8
|
+
- 'toon.gemspec'
|
|
9
|
+
|
|
10
|
+
Metrics/AbcSize:
|
|
11
|
+
Exclude:
|
|
12
|
+
- 'lib/toonify/decoder.rb'
|
|
13
|
+
- 'examples/**/*'
|
|
14
|
+
|
|
15
|
+
Metrics/CyclomaticComplexity:
|
|
16
|
+
Exclude:
|
|
17
|
+
- 'lib/toonify/decoder.rb'
|
|
18
|
+
|
|
19
|
+
Metrics/MethodLength:
|
|
20
|
+
Exclude:
|
|
21
|
+
- 'lib/toonify/decoder.rb'
|
|
22
|
+
- 'lib/toonify/encoder.rb'
|
|
23
|
+
- 'examples/**/*'
|
|
24
|
+
|
|
25
|
+
Metrics/PerceivedComplexity:
|
|
26
|
+
Exclude:
|
|
27
|
+
- 'lib/toonify/decoder.rb'
|
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
# Specify your gem's dependencies in toon.gemspec
|
|
6
|
+
gemspec
|
|
7
|
+
|
|
8
|
+
gem 'rake', '~> 13.0'
|
|
9
|
+
|
|
10
|
+
# Development and test dependencies
|
|
11
|
+
group :development, :test do
|
|
12
|
+
gem 'rspec', '~> 3.0'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gem 'rubocop', '~> 1.81', groups: %i[development test]
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
toonify (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
ast (2.4.3)
|
|
10
|
+
diff-lcs (1.6.2)
|
|
11
|
+
json (2.16.0)
|
|
12
|
+
language_server-protocol (3.17.0.5)
|
|
13
|
+
lint_roller (1.1.0)
|
|
14
|
+
parallel (1.27.0)
|
|
15
|
+
parser (3.3.10.0)
|
|
16
|
+
ast (~> 2.4.1)
|
|
17
|
+
racc
|
|
18
|
+
prism (1.6.0)
|
|
19
|
+
racc (1.8.1)
|
|
20
|
+
rainbow (3.1.1)
|
|
21
|
+
rake (13.3.1)
|
|
22
|
+
regexp_parser (2.11.3)
|
|
23
|
+
rspec (3.13.2)
|
|
24
|
+
rspec-core (~> 3.13.0)
|
|
25
|
+
rspec-expectations (~> 3.13.0)
|
|
26
|
+
rspec-mocks (~> 3.13.0)
|
|
27
|
+
rspec-core (3.13.6)
|
|
28
|
+
rspec-support (~> 3.13.0)
|
|
29
|
+
rspec-expectations (3.13.5)
|
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
31
|
+
rspec-support (~> 3.13.0)
|
|
32
|
+
rspec-mocks (3.13.7)
|
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
34
|
+
rspec-support (~> 3.13.0)
|
|
35
|
+
rspec-support (3.13.6)
|
|
36
|
+
rubocop (1.81.7)
|
|
37
|
+
json (~> 2.3)
|
|
38
|
+
language_server-protocol (~> 3.17.0.2)
|
|
39
|
+
lint_roller (~> 1.1.0)
|
|
40
|
+
parallel (~> 1.10)
|
|
41
|
+
parser (>= 3.3.0.2)
|
|
42
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
43
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
44
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
45
|
+
ruby-progressbar (~> 1.7)
|
|
46
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
47
|
+
rubocop-ast (1.48.0)
|
|
48
|
+
parser (>= 3.3.7.2)
|
|
49
|
+
prism (~> 1.4)
|
|
50
|
+
ruby-progressbar (1.13.0)
|
|
51
|
+
unicode-display_width (3.2.0)
|
|
52
|
+
unicode-emoji (~> 4.1)
|
|
53
|
+
unicode-emoji (4.1.0)
|
|
54
|
+
|
|
55
|
+
PLATFORMS
|
|
56
|
+
arm64-darwin-24
|
|
57
|
+
|
|
58
|
+
DEPENDENCIES
|
|
59
|
+
rake (~> 13.0)
|
|
60
|
+
rspec (~> 3.0)
|
|
61
|
+
rubocop (~> 1.81)
|
|
62
|
+
toonify!
|
|
63
|
+
|
|
64
|
+
BUNDLED WITH
|
|
65
|
+
2.7.2
|
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# TOON: Token-Oriented Object Notation
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
**TOON** (Token-Oriented Object Notation) is a lightweight, human-readable data serialization format designed to be **token-efficient for Large Language Models (LLMs)** while remaining easy for humans to read and write.
|
|
6
|
+
|
|
7
|
+
It serves as a concise alternative to JSON, removing syntactic noise (like excessive quotes, braces, and brackets) to reduce token usage and improve clarity.
|
|
8
|
+
|
|
9
|
+
## 🚀 Why TOON?
|
|
10
|
+
|
|
11
|
+
### 1. Token Efficiency for AI/LLMs
|
|
12
|
+
JSON is verbose. For LLMs (like GPT-4, Claude, Gemini), every character counts. TOON reduces the token footprint by eliminating structural overhead, which can lead to:
|
|
13
|
+
* **Lower Costs**: Fewer tokens processed means lower API bills.
|
|
14
|
+
* **Larger Context**: Fit more data into the model's context window.
|
|
15
|
+
* **Faster Generation**: Less syntax for the model to generate.
|
|
16
|
+
|
|
17
|
+
### 2. Human Readability
|
|
18
|
+
TOON looks like a clean configuration file or a summary report. It uses significant whitespace and minimal punctuation, making it ideal for:
|
|
19
|
+
* Logs and debug output.
|
|
20
|
+
* Configuration files.
|
|
21
|
+
* Data summaries for dashboards.
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
Add this line to your application's Gemfile:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem 'toonify'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
And then execute:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install it yourself as:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
$ gem install toonify
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 💻 Usage
|
|
44
|
+
|
|
45
|
+
The `toonify` gem provides a simple API to convert between JSON and TOON.
|
|
46
|
+
|
|
47
|
+
### Basic Conversion
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require 'toonify'
|
|
51
|
+
|
|
52
|
+
# Input JSON
|
|
53
|
+
json_data = '{"name": "Alice", "role": "Engineer", "active": true}'
|
|
54
|
+
|
|
55
|
+
# Encode JSON -> TOON
|
|
56
|
+
toon_output = Toon.encode(json_data)
|
|
57
|
+
puts toon_output
|
|
58
|
+
# Output:
|
|
59
|
+
# name: Alice
|
|
60
|
+
# role: Engineer
|
|
61
|
+
# active: true
|
|
62
|
+
|
|
63
|
+
# Decode TOON -> JSON
|
|
64
|
+
json_output = Toon.decode(toon_output)
|
|
65
|
+
puts json_output
|
|
66
|
+
# Output: {"name":"Alice","role":"Engineer","active":true}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Handling Complex Data
|
|
70
|
+
|
|
71
|
+
TOON shines with nested structures and arrays. It automatically detects tabular data and formats it concisely.
|
|
72
|
+
|
|
73
|
+
### Error Handling
|
|
74
|
+
|
|
75
|
+
The converter is strict about input types to ensure reliability.
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
begin
|
|
79
|
+
Toon.encode('invalid json')
|
|
80
|
+
rescue ArgumentError => e
|
|
81
|
+
puts e.message # => "Invalid JSON input"
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 🔍 Format Specification
|
|
86
|
+
|
|
87
|
+
TOON uses a few simple rules:
|
|
88
|
+
|
|
89
|
+
* **Key-Value**: `key: value`
|
|
90
|
+
* **Nested Objects**: Indented blocks (YAML-style).
|
|
91
|
+
* **Primitive Arrays**: `key[count]: val1,val2,val3`
|
|
92
|
+
* **Object Arrays (Tabular)**: `key[count]{headers}:` followed by CSV-like rows.
|
|
93
|
+
|
|
94
|
+
## 🤝 Contributing
|
|
95
|
+
|
|
96
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/ran010/toonify](https://github.com/ran010/toonify).
|
|
97
|
+
|
|
98
|
+
## 📄 License
|
|
99
|
+
|
|
100
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
task default: %i[]
|
|
5
|
+
|
|
6
|
+
# RSpec task
|
|
7
|
+
begin
|
|
8
|
+
require 'rspec/core/rake_task'
|
|
9
|
+
|
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
+
task default: %i[spec]
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# rspec not available; leave default task as-is
|
|
14
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative '../lib/toonify'
|
|
7
|
+
|
|
8
|
+
# ------------------------------------------------------------------------------
|
|
9
|
+
# Configuration
|
|
10
|
+
# ------------------------------------------------------------------------------
|
|
11
|
+
API_KEY = 'api-key'
|
|
12
|
+
MODEL_NAME = 'gemini-2.0-flash'
|
|
13
|
+
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/#{MODEL_NAME}:countTokens?key=#{API_KEY}"
|
|
14
|
+
|
|
15
|
+
if API_KEY.nil? || API_KEY.empty?
|
|
16
|
+
puts 'Please set the GEMINI_API_KEY environment variable.'
|
|
17
|
+
puts "Example: export GEMINI_API_KEY='your_api_key'"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ------------------------------------------------------------------------------
|
|
22
|
+
# Sample Data (Complex Nested Structure)
|
|
23
|
+
# ------------------------------------------------------------------------------
|
|
24
|
+
data = {
|
|
25
|
+
products: [
|
|
26
|
+
{
|
|
27
|
+
id: 101,
|
|
28
|
+
name: 'Super Gadget',
|
|
29
|
+
features: { wireless: true, battery: '24h', colors: %w[black white] },
|
|
30
|
+
reviews: [
|
|
31
|
+
{ user: 'alice', rating: 5, comment: 'Amazing battery life!' },
|
|
32
|
+
{ user: 'bob', rating: 4, comment: 'Good, but expensive.' }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 102,
|
|
37
|
+
name: 'Budget Widget',
|
|
38
|
+
features: { wireless: false, battery: '12h', colors: ['gray'] },
|
|
39
|
+
reviews: [
|
|
40
|
+
{ user: 'charlie', rating: 3, comment: 'It works, I guess.' }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
metadata: {
|
|
45
|
+
generated_at: '2023-10-27T10:00:00Z',
|
|
46
|
+
source: 'inventory_system',
|
|
47
|
+
tags: %w[electronics gadgets sale]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------------------
|
|
52
|
+
# Conversion
|
|
53
|
+
# ------------------------------------------------------------------------------
|
|
54
|
+
json_string = JSON.pretty_generate(data)
|
|
55
|
+
toon_string = Toon.encode(json_string)
|
|
56
|
+
|
|
57
|
+
puts "--- JSON Content (#{json_string.length} chars) ---"
|
|
58
|
+
puts json_string
|
|
59
|
+
puts "\n--- TOON Content (#{toon_string.length} chars) ---"
|
|
60
|
+
puts toon_string
|
|
61
|
+
puts "\n--------------------------------------------------"
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------------------
|
|
64
|
+
# Token Counting Helper
|
|
65
|
+
# ------------------------------------------------------------------------------
|
|
66
|
+
def count_tokens(text_content)
|
|
67
|
+
# 1. Prepare URI and HTTP client
|
|
68
|
+
uri = URI(API_URL)
|
|
69
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
|
+
http.use_ssl = true # Use SSL for HTTPS connection
|
|
71
|
+
|
|
72
|
+
# 2. Build the POST request
|
|
73
|
+
request = Net::HTTP::Post.new(uri)
|
|
74
|
+
request['Content-Type'] = 'application/json'
|
|
75
|
+
|
|
76
|
+
# 3. Prepare the JSON body structure
|
|
77
|
+
request_body = {
|
|
78
|
+
contents: [{
|
|
79
|
+
parts: [{
|
|
80
|
+
text: text_content
|
|
81
|
+
}]
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Set the body as a JSON string
|
|
86
|
+
request.body = request_body.to_json
|
|
87
|
+
|
|
88
|
+
# 4. Send the request and handle the response
|
|
89
|
+
puts "Sending request to: #{API_URL}"
|
|
90
|
+
response = http.request(request)
|
|
91
|
+
|
|
92
|
+
if response.code == '200'
|
|
93
|
+
response_data = JSON.parse(response.body)
|
|
94
|
+
token_count = response_data['totalTokens']
|
|
95
|
+
|
|
96
|
+
puts "\n✅ Successfully counted tokens."
|
|
97
|
+
puts "Text: \"#{text_content}\""
|
|
98
|
+
puts "Total Tokens: #{token_count}"
|
|
99
|
+
|
|
100
|
+
token_count
|
|
101
|
+
else
|
|
102
|
+
puts "\n❌ Error calling Gemini API (HTTP #{response.code}):"
|
|
103
|
+
puts response.body
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------------------
|
|
109
|
+
# Comparison
|
|
110
|
+
# ------------------------------------------------------------------------------
|
|
111
|
+
puts "Calculating tokens with #{MODEL_NAME}..."
|
|
112
|
+
|
|
113
|
+
json_tokens = count_tokens(json_string)
|
|
114
|
+
toon_tokens = count_tokens(toon_string)
|
|
115
|
+
|
|
116
|
+
if json_tokens && toon_tokens
|
|
117
|
+
diff = json_tokens - toon_tokens
|
|
118
|
+
percent = (diff.to_f / json_tokens * 100).round(2)
|
|
119
|
+
|
|
120
|
+
puts "\nToken Usage Comparison:"
|
|
121
|
+
puts "JSON Tokens: #{json_tokens}"
|
|
122
|
+
puts "TOON Tokens: #{toon_tokens}"
|
|
123
|
+
puts "Savings: #{diff} tokens (#{percent}%)"
|
|
124
|
+
else
|
|
125
|
+
puts 'Failed to retrieve token counts.'
|
|
126
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Toonify
|
|
6
|
+
# Parses TOON (Token Oriented Object Notation) back into Ruby objects or JSON.
|
|
7
|
+
# Supports the subset produced by Toonify::Encoder:
|
|
8
|
+
# - key: value
|
|
9
|
+
# - key: (nested hash with indented lines)
|
|
10
|
+
# - key[n]: v1,v2,... (array of primitives)
|
|
11
|
+
# - key[n]{col1,col2}: (tabular array of hashes; rows are indented)
|
|
12
|
+
class Decoder
|
|
13
|
+
INDENT = ' '
|
|
14
|
+
|
|
15
|
+
def initialize(toon_string)
|
|
16
|
+
@lines = toon_string.to_s.lines.map(&:rstrip)
|
|
17
|
+
@index = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
parse_top_level_hash
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def decode
|
|
25
|
+
JSON.generate(parse)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse(toon_string)
|
|
29
|
+
new(toon_string).parse
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.decode(toon_string)
|
|
33
|
+
new(toon_string).decode
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse_top_level_hash
|
|
39
|
+
result = {}
|
|
40
|
+
|
|
41
|
+
while @index < @lines.length
|
|
42
|
+
line = @lines[@index]
|
|
43
|
+
@index += 1
|
|
44
|
+
|
|
45
|
+
next if line.nil? || line.strip.empty?
|
|
46
|
+
|
|
47
|
+
if (m = line.match(/^([^\[:{]+)\[(\d+)\]\{([^}]+)\}:$/))
|
|
48
|
+
key = m[1].strip
|
|
49
|
+
cols = m[3].split(',').map(&:strip)
|
|
50
|
+
|
|
51
|
+
rows = []
|
|
52
|
+
while peek_indented_line?
|
|
53
|
+
row_line = @lines[@index].lstrip
|
|
54
|
+
@index += 1
|
|
55
|
+
values = row_line.split(',').map { |t| parse_token(t) }
|
|
56
|
+
row = {}
|
|
57
|
+
cols.each_with_index do |col, i|
|
|
58
|
+
row[col] = values[i]
|
|
59
|
+
end
|
|
60
|
+
rows << row
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
result[key] = rows
|
|
64
|
+
|
|
65
|
+
elsif (m = line.match(/^([^\[]+)\[(\d+)\]:\s*(.*)$/))
|
|
66
|
+
key = m[1].strip
|
|
67
|
+
rest = m[3].strip
|
|
68
|
+
arr = rest.empty? ? [] : rest.split(',').map { |t| parse_token(t) }
|
|
69
|
+
result[key] = arr
|
|
70
|
+
|
|
71
|
+
elsif (m = line.match(/^([^:]+):\s*$/)) && !line.include?('{')
|
|
72
|
+
key = m[1].strip
|
|
73
|
+
nested = {}
|
|
74
|
+
while peek_indented_line?
|
|
75
|
+
nested_line = @lines[@index].lstrip
|
|
76
|
+
@index += 1
|
|
77
|
+
next unless (mm = nested_line.match(/^([^:]+):\s*(.*)$/))
|
|
78
|
+
|
|
79
|
+
nkey = mm[1].strip
|
|
80
|
+
nval = mm[2].strip
|
|
81
|
+
nested[nkey] = parse_token(nval)
|
|
82
|
+
end
|
|
83
|
+
result[key] = nested
|
|
84
|
+
|
|
85
|
+
elsif (m = line.match(/^([^:]+):\s*(.*)$/))
|
|
86
|
+
key = m[1].strip
|
|
87
|
+
val = m[2].strip
|
|
88
|
+
result[key] = parse_token(val)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def peek_indented_line?
|
|
96
|
+
return false if @index >= @lines.length
|
|
97
|
+
|
|
98
|
+
next_line = @lines[@index]
|
|
99
|
+
return false if next_line.nil?
|
|
100
|
+
|
|
101
|
+
next_line.start_with?(INDENT)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parse_token(token)
|
|
105
|
+
t = token.to_s.strip
|
|
106
|
+
return nil if t == ''
|
|
107
|
+
return nil if t.downcase == 'null'
|
|
108
|
+
return true if t.downcase == 'true'
|
|
109
|
+
return false if t.downcase == 'false'
|
|
110
|
+
|
|
111
|
+
if t.match?(/\A-?\d+\z/)
|
|
112
|
+
return t.to_i
|
|
113
|
+
elsif t.match?(/\A-?\d+\.\d+\z/)
|
|
114
|
+
return t.to_f
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
return t[1..-2].gsub('\\"', '"') if t.start_with?('"') && t.end_with?('"') && t.length >= 2
|
|
118
|
+
|
|
119
|
+
t
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toonify
|
|
4
|
+
# Encodes Ruby data structures into TOON format.
|
|
5
|
+
# Handles primitives, hashes, arrays, and nested structures.
|
|
6
|
+
class Encoder
|
|
7
|
+
INDENT = ' '
|
|
8
|
+
|
|
9
|
+
# Creates a new Encoder instance.
|
|
10
|
+
# @param data [Hash, Object] The data to encode.
|
|
11
|
+
def initialize(data)
|
|
12
|
+
@data = data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Encodes the data into TOON format.
|
|
16
|
+
# @return [String] The TOON-formatted output.
|
|
17
|
+
def encode
|
|
18
|
+
result = format_hash(@data, 0)
|
|
19
|
+
result.join("\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Formats a value based on its type.
|
|
25
|
+
# @param value [Object] The value to format.
|
|
26
|
+
# @param depth [Integer] Current indentation depth.
|
|
27
|
+
# @return [String] The formatted value.
|
|
28
|
+
def format_value(value, depth)
|
|
29
|
+
case value
|
|
30
|
+
when Hash
|
|
31
|
+
format_hash(value, depth)
|
|
32
|
+
when Array
|
|
33
|
+
format_array(value, depth)
|
|
34
|
+
else
|
|
35
|
+
value.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Formats a Hash into TOON format.
|
|
40
|
+
# @param hash [Hash] The hash to format.
|
|
41
|
+
# @param depth [Integer] Current indentation depth.
|
|
42
|
+
# @return [Array<String>] Array of formatted lines.
|
|
43
|
+
def format_hash(hash, depth)
|
|
44
|
+
lines = []
|
|
45
|
+
|
|
46
|
+
hash.each do |key, value|
|
|
47
|
+
case value
|
|
48
|
+
when Hash
|
|
49
|
+
lines << "#{key}:"
|
|
50
|
+
value.each do |k, v|
|
|
51
|
+
lines << "#{INDENT}#{k}: #{format_value(v, depth + 1)}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
when Array
|
|
55
|
+
lines << if uniform_hash_array?(value)
|
|
56
|
+
format_tabular_array(key.to_s, value, depth)
|
|
57
|
+
else
|
|
58
|
+
"#{key}[#{value.size}]: #{value.join(',')}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
else
|
|
62
|
+
lines << "#{key}: #{format_value(value, depth)}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Formats an array.
|
|
70
|
+
# @param array [Array] The array to format.
|
|
71
|
+
# @param depth [Integer] Current indentation depth.
|
|
72
|
+
# @return [String] The formatted array.
|
|
73
|
+
def format_array(array, depth)
|
|
74
|
+
if uniform_hash_array?(array)
|
|
75
|
+
format_tabular_array('array', array, depth)
|
|
76
|
+
else
|
|
77
|
+
array.map { |item| format_value(item, depth) }.join(',')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Checks if an array contains uniform hashes.
|
|
82
|
+
# @param array [Array] The array to check.
|
|
83
|
+
# @return [Boolean] True if uniform hashes, false otherwise.
|
|
84
|
+
def uniform_hash_array?(array)
|
|
85
|
+
return false if array.empty? || !array.all? { |item| item.is_a?(Hash) }
|
|
86
|
+
|
|
87
|
+
first_keys = array.first.keys.sort
|
|
88
|
+
array.all? { |item| item.keys.sort == first_keys }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Formats an array of uniform hashes in tabular (CSV-like) format.
|
|
92
|
+
# @param key [String] The array key name.
|
|
93
|
+
# @param array [Array] The array of hashes.
|
|
94
|
+
# @param _depth [Integer] Current indentation depth (unused).
|
|
95
|
+
# @return [String] The formatted tabular array.
|
|
96
|
+
def format_tabular_array(key, array, _depth)
|
|
97
|
+
keys = array.first.keys.sort
|
|
98
|
+
header = keys.join(',')
|
|
99
|
+
|
|
100
|
+
formatted_lines = ["#{key}[#{array.size}]{#{header}}:", *array.map do |row|
|
|
101
|
+
values = keys.map { |k| row[k].to_s }
|
|
102
|
+
"#{INDENT}#{values.join(',')}"
|
|
103
|
+
end]
|
|
104
|
+
formatted_lines.join("\n")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/toonify.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'toonify/version'
|
|
5
|
+
require_relative 'toonify/encoder'
|
|
6
|
+
require_relative 'toonify/decoder'
|
|
7
|
+
|
|
8
|
+
module Toonify
|
|
9
|
+
class Toon
|
|
10
|
+
# Public class method for easy conversion, useful for quick access.
|
|
11
|
+
# @param json_string [String] The raw JSON input.
|
|
12
|
+
# @return [String] The converted custom text output.
|
|
13
|
+
# @raise [ArgumentError] if the JSON input is invalid.
|
|
14
|
+
def self.encode(json_string)
|
|
15
|
+
new(json_string).encode
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Initializes the converter with the raw JSON string.
|
|
19
|
+
# @param json_string [String] The raw JSON input.
|
|
20
|
+
def initialize(json_string)
|
|
21
|
+
@json_string = json_string
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parses the JSON and performs the transformation using the Encoder.
|
|
25
|
+
# @return [String] The converted custom text output.
|
|
26
|
+
def encode
|
|
27
|
+
data = JSON.parse(@json_string)
|
|
28
|
+
raise ArgumentError, 'Invalid JSON input' unless data.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
Encoder.new(data).encode
|
|
31
|
+
rescue JSON::ParserError, TypeError
|
|
32
|
+
raise ArgumentError, 'Invalid JSON input'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Convenience: decode a TOON string back to JSON string
|
|
36
|
+
def self.decode(toon_string)
|
|
37
|
+
Decoder.decode(toon_string)
|
|
38
|
+
rescue StandardError
|
|
39
|
+
@json_string
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toonify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ranjan
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Toonify converts JSON data into a custom human-readable text format called
|
|
13
|
+
TOON.
|
|
14
|
+
email:
|
|
15
|
+
- ranjanbajra@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- ".rspec"
|
|
21
|
+
- ".rubocop.yml"
|
|
22
|
+
- Gemfile
|
|
23
|
+
- Gemfile.lock
|
|
24
|
+
- README.md
|
|
25
|
+
- Rakefile
|
|
26
|
+
- examples/gemini_token_comparison.rb
|
|
27
|
+
- lib/toonify.rb
|
|
28
|
+
- lib/toonify/decoder.rb
|
|
29
|
+
- lib/toonify/encoder.rb
|
|
30
|
+
- lib/toonify/version.rb
|
|
31
|
+
homepage: https://github.com/ran010/toonify
|
|
32
|
+
licenses: []
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/ran010/toonify
|
|
35
|
+
source_code_uri: https://github.com/ran010/toonify
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: 2.6.0
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.7.2
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: A simple JSON to custom text format converter.
|
|
54
|
+
test_files: []
|