ruby-toon 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7f006a20c57bdd904663ccfd5294101e625934bc5a1df303a6f2cf471f3745df
4
+ data.tar.gz: 80e3c4cd371b9eca3c47855bc79fdf67102e6c04272c69c3c1f752957c240e5d
5
+ SHA512:
6
+ metadata.gz: 6c6ef31232a2209e64815eae5f25f9bf195caee071323777eeac4a4217b45ae3937c7a8cb937d5dd60bc46f4b6c3ca0b757b0e61de58d314e67454d03f3a5b9a
7
+ data.tar.gz: 1f7ee09ca1533d1ac7cb4ce3b7233f27dae0389ddfda99e9771b3087918c47e5528e858d83ddcfe69496973412a846107c80d01e6f3995a9953da8d658788870
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anil Yanduri
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,260 @@
1
+ ````markdown
2
+ # 🎋 Toon — Token-Oriented Object Notation for Ruby
3
+
4
+ `toon` is a Ruby implementation of **TOON (Token-Oriented Object Notation)** —
5
+ a compact, readable, indentation-based data format designed for humans *and* machines.
6
+
7
+ This gem provides:
8
+
9
+ - A **TOON encoder** (Ruby → TOON)
10
+ - A **TOON decoder** (TOON → Ruby)
11
+ - A **CLI** (`bin/toon`) for converting TOON ↔ JSON
12
+ - Optional **ActiveSupport integration** (`Object#to_toon`)
13
+ - Full RSpec test suite
14
+
15
+ ---
16
+
17
+ ## ✨ Features
18
+
19
+ ### ✔ Encode Ruby objects → TOON
20
+ ```ruby
21
+ Toon.generate({ "name" => "Alice", "age" => 30 })
22
+ ````
23
+
24
+ Produces:
25
+
26
+ ```
27
+ name:Alice
28
+ age:30
29
+ ```
30
+
31
+ ### ✔ Decode TOON → Ruby
32
+
33
+ ```ruby
34
+ Toon.parse("name:Alice\nage:30")
35
+ ```
36
+
37
+ Returns:
38
+
39
+ ```ruby
40
+ { "name" => "Alice", "age" => 30 }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 🧩 Arrays
46
+
47
+ ### Nested arrays (encoder output)
48
+
49
+ ```
50
+ colors:
51
+ [3]:
52
+ red
53
+ green
54
+ blue
55
+ ```
56
+
57
+ ### Flat arrays (user input)
58
+
59
+ ```
60
+ colors[3]:
61
+ red
62
+ green
63
+ blue
64
+ ```
65
+
66
+ Both decode correctly.
67
+
68
+ ---
69
+
70
+ ## 📊 Tabular Arrays
71
+
72
+ ### Nested tabular (encoder output)
73
+
74
+ ```
75
+ users:
76
+ [2]{id,name}:
77
+ 1,A
78
+ 2,B
79
+ ```
80
+
81
+ → numeric fields parsed (`id` becomes integer)
82
+
83
+ ### Flat tabular (user input)
84
+
85
+ ```
86
+ users[2]{id,name}:
87
+ 1,Alice
88
+ 2,Bob
89
+ ```
90
+
91
+ → fields remain **strings**
92
+
93
+ ---
94
+
95
+ ## ⚙️ ActiveSupport Integration
96
+
97
+ If ActiveSupport is installed:
98
+
99
+ ```ruby
100
+ require "active_support"
101
+ require "toon"
102
+
103
+ {a: 1}.to_toon
104
+ # => "a:1\n"
105
+ ```
106
+
107
+ Adds `Object#to_toon` for convenience.
108
+
109
+ ---
110
+
111
+ ## 🚀 Installation
112
+
113
+ Gem coming soon. For now:
114
+
115
+ ```bash
116
+ git clone <your-repo-url>
117
+ cd toon
118
+ bundle install
119
+ ```
120
+
121
+ Use locally:
122
+
123
+ ```ruby
124
+ require_relative "lib/toon"
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 🧰 CLI Usage
130
+
131
+ ### Encode JSON → TOON
132
+
133
+ ```bash
134
+ echo '{"name":"Alice","age":30}' | bin/toon --encode
135
+ ```
136
+
137
+ ### Decode TOON → JSON
138
+
139
+ ```bash
140
+ echo "name:Alice\nage:30" | bin/toon --decode
141
+ ```
142
+
143
+ ### Read from STDIN automatically
144
+
145
+ ```bash
146
+ bin/toon --encode < input.json
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🧪 Running Tests
152
+
153
+ Ensure CLI is executable:
154
+
155
+ ```bash
156
+ chmod +x bin/toon
157
+ ```
158
+
159
+ Run all tests:
160
+
161
+ ```bash
162
+ bundle exec rspec
163
+ ```
164
+
165
+ Tests include:
166
+
167
+ * Encoder specs
168
+ * Decoder specs
169
+ * CLI specs
170
+ * ActiveSupport specs
171
+
172
+ ---
173
+
174
+ ## 📚 Supported TOON Grammar (Current)
175
+
176
+ ### Key-value
177
+
178
+ ```
179
+ key:value
180
+ ```
181
+
182
+ ### Nested objects
183
+
184
+ ```
185
+ user:
186
+ name:Alice
187
+ age:30
188
+ ```
189
+
190
+ ### Primitive arrays
191
+
192
+ ```
193
+ colors:
194
+ [3]:
195
+ red
196
+ green
197
+ blue
198
+ ```
199
+
200
+ Flat form:
201
+
202
+ ```
203
+ colors[3]:
204
+ red
205
+ green
206
+ blue
207
+ ```
208
+
209
+ ### Tabular arrays
210
+
211
+ ```
212
+ users:
213
+ [2]{id,name}:
214
+ 1,A
215
+ 2,B
216
+ ```
217
+
218
+ Flat form:
219
+
220
+ ```
221
+ users[2]{id,name}:
222
+ 1,A
223
+ 2,B
224
+ ```
225
+
226
+ ---
227
+
228
+ ## ⚠️ Error Handling
229
+
230
+ Malformed input (e.g., missing indentation):
231
+
232
+ ```
233
+ Malformed TOON: array header '[3]:' must be under a key (e.g., 'colors:')
234
+ ```
235
+
236
+ Decoder stops with a friendly `Toon::Error`.
237
+
238
+ ---
239
+
240
+ ## 🗺️ Roadmap
241
+
242
+ * Multiline values
243
+ * Quoted strings
244
+ * Mixed-type arrays
245
+ * Strict vs non-strict modes
246
+ * Streaming decoder
247
+ * Schema validation
248
+ * Ruby gem release
249
+
250
+ ---
251
+
252
+ ## ❤️ Contributing
253
+
254
+ PRs and issues welcome!
255
+
256
+ ---
257
+
258
+ ## 📝 License
259
+
260
+ MIT
@@ -0,0 +1,12 @@
1
+ # Optional ActiveSupport integration
2
+ begin
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object'
5
+ class Object
6
+ def to_toon(**opts)
7
+ Toon.generate(self, **opts)
8
+ end
9
+ end
10
+ rescue LoadError
11
+ # no activesupport available
12
+ end
@@ -0,0 +1,182 @@
1
+ module Toon
2
+ module Decoder
3
+ module_function
4
+
5
+ def parse(str, **opts)
6
+ lines = str.gsub("\r\n", "\n").split("\n")
7
+ parse_lines(lines)
8
+ end
9
+
10
+ def load(io, **opts)
11
+ parse(io.read, **opts)
12
+ end
13
+
14
+ def parse_lines(lines)
15
+ root = {}
16
+ stack = [ { indent: -1, obj: root, parent: nil, key: nil } ]
17
+
18
+ i = 0
19
+ while i < lines.length
20
+ raw = lines[i].rstrip
21
+ i += 1
22
+
23
+ next if raw.strip.empty? || raw.strip.start_with?('#')
24
+
25
+ indent = leading_spaces(raw)
26
+ content = raw.strip
27
+
28
+ # Fix indentation
29
+ while indent <= stack.last[:indent]
30
+ stack.pop
31
+ end
32
+
33
+ current = stack.last
34
+ parent_obj = current[:obj]
35
+
36
+ # ============================================================
37
+ # FLAT TABULAR ARRAY: users[2]{id,name}:
38
+ # ============================================================
39
+ if m = content.match(/^(\w+)\[(\d+)\]\{([^}]*)\}:$/)
40
+ key = m[1]
41
+ count = m[2].to_i
42
+ fields = m[3].split(",").map(&:strip)
43
+
44
+ rows = []
45
+ count.times do
46
+ row = lines[i]&.strip
47
+ i += 1
48
+ next unless row
49
+ values = split_row(row.strip)
50
+ rows << Hash[fields.zip(values)]
51
+ end
52
+
53
+ parent_obj[key] = rows
54
+ next
55
+ end
56
+
57
+ # ============================================================
58
+ # FLAT PRIMITIVE ARRAY: colors[3]:
59
+ # ============================================================
60
+ if m = content.match(/^(\w+)\[(\d+)\]:$/)
61
+ key = m[1]
62
+ count = m[2].to_i
63
+
64
+ values = []
65
+ count.times do
66
+ row = lines[i]&.strip
67
+ i += 1
68
+ next unless row
69
+ values << parse_scalar(row)
70
+ end
71
+
72
+ parent_obj[key] = values
73
+ next
74
+ end
75
+
76
+ # ============================================================
77
+ # NESTED OBJECT KEY: key:
78
+ # ============================================================
79
+ if m = content.match(/^(\w+):$/)
80
+ key = m[1]
81
+ new_obj = {}
82
+
83
+ parent_obj[key] = new_obj
84
+ stack << { indent: indent, obj: new_obj, parent: parent_obj, key: key }
85
+ next
86
+ end
87
+
88
+ # Refresh frame for nested array parsing
89
+ frame = stack.last
90
+ parent = frame[:parent]
91
+ key = frame[:key]
92
+
93
+ # ============================================================
94
+ # NESTED TABULAR ARRAY:
95
+ # users:
96
+ # [2]{id,name}:
97
+ # ============================================================
98
+ if m = content.match(/^\[(\d+)\]\{([^}]*)\}:$/)
99
+ count = m[1].to_i
100
+ fields = m[2].split(',').map(&:strip)
101
+
102
+ rows = []
103
+ count.times do
104
+ row = lines[i]&.strip
105
+ i += 1
106
+ next unless row
107
+ values = split_row(row).map { |v| parse_scalar(v) }
108
+ rows << Hash[fields.zip(values)]
109
+ end
110
+
111
+ parent[key] = rows
112
+ stack.pop
113
+ next
114
+ end
115
+
116
+ # ============================================================
117
+ # NESTED PRIMITIVE ARRAY:
118
+ # colors:
119
+ # [3]:
120
+ # ============================================================
121
+ if m = content.match(/^\[(\d+)\]:$/)
122
+ count = m[1].to_i
123
+
124
+ if key.nil?
125
+ raise Toon::Error, "Malformed TOON: array header '#{content}' must be under a key (e.g., 'colors:')"
126
+ end
127
+
128
+ values = []
129
+ count.times do
130
+ row = lines[i]&.strip
131
+ i += 1
132
+ next unless row
133
+ values << parse_scalar(row)
134
+ end
135
+
136
+ parent[key] = values
137
+ stack.pop
138
+ next
139
+ end
140
+
141
+ # ============================================================
142
+ # SIMPLE KEY: VALUE
143
+ # ============================================================
144
+ if m = content.match(/^(\w+):\s*(.*)$/)
145
+ k = m[1]
146
+ v = parse_scalar(m[2])
147
+ parent_obj[k] = v
148
+ next
149
+ end
150
+ end
151
+
152
+ root
153
+ end
154
+
155
+
156
+
157
+ def leading_spaces(line)
158
+ line[/^ */].length
159
+ end
160
+
161
+ def parse_scalar(str)
162
+ # strip quotes
163
+ if str == 'null' then nil
164
+ elsif str == 'true' then true
165
+ elsif str == 'false' then false
166
+ elsif str.match?(/^".*"$/)
167
+ str[1..-2].gsub('\\"','"')
168
+ elsif str =~ /^-?\d+$/
169
+ str.to_i
170
+ elsif str =~ /^-?\d+\.\d+$/
171
+ str.to_f
172
+ else
173
+ str
174
+ end
175
+ end
176
+
177
+ def split_row(row)
178
+ # simplistic split on comma that ignores quoted commas - naive
179
+ row.scan(/(?:\"([^\"]*)\"|([^,]+))(?:,|$)/).map { |m| (m[0] || m[1]).to_s.strip }
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,128 @@
1
+ require 'stringio'
2
+ module Toon
3
+ module Encoder
4
+ module_function
5
+
6
+ DEFAULT_OPTIONS = {
7
+ delimiter: ',',
8
+ indent: 2,
9
+ compact_arrays: true, # try to use tabular form when possible
10
+ pretty: false
11
+ }
12
+
13
+ def generate(obj, **opts)
14
+ options = DEFAULT_OPTIONS.merge(opts)
15
+ io = StringIO.new
16
+ write_object(io, obj, 0, options)
17
+ io.string
18
+ end
19
+
20
+ def pretty_generate(obj, **opts)
21
+ generate(obj, **opts.merge(pretty: true))
22
+ end
23
+
24
+ def dump(obj, io = STDOUT, **opts)
25
+ str = generate(obj, **opts)
26
+ io.write(str)
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ def write_object(io, obj, level, options)
33
+ case obj
34
+ when Hash
35
+ obj.each do |k, v|
36
+ write_key(io, k, level)
37
+ if simple_value?(v)
38
+ io.write(format_simple(v))
39
+ io.write("\n")
40
+ else
41
+ io.write("\n")
42
+ write_object(io, v, level + 1, options)
43
+ end
44
+ end
45
+ when Array
46
+ # Try to detect uniform objects for tabular style
47
+ if options[:compact_arrays] && array_uniform_hashes?(obj)
48
+ write_tabular(io, obj, level, options)
49
+ else
50
+ write_list(io, obj, level, options)
51
+ end
52
+ else
53
+ # scalar
54
+ io.write(indent(level))
55
+ io.write(format_simple(obj))
56
+ io.write("\n")
57
+ end
58
+ end
59
+
60
+ def write_key(io, key, level)
61
+ io.write(indent(level))
62
+ io.write("#{key}:")
63
+ end
64
+
65
+ def write_tabular(io, arr, level, options)
66
+ keys = (arr.map(&:keys).reduce(:|) || []).uniq
67
+ io.write(indent(level))
68
+ io.write("[#{arr.length}]{#{keys.join(',')}}:\n")
69
+ arr.each do |row|
70
+ io.write(indent(level + 1))
71
+ vals = keys.map { |k| format_simple(row[k]) }
72
+ io.write(vals.join(options[:delimiter]))
73
+ io.write("\n")
74
+ end
75
+ end
76
+
77
+ def write_list(io, arr, level, options)
78
+ io.write(indent(level))
79
+ io.write("[#{arr.length}]:\n")
80
+ arr.each do |el|
81
+ io.write(indent(level + 1))
82
+ if simple_value?(el)
83
+ io.write(format_simple(el))
84
+ io.write("\n")
85
+ else
86
+ # nested object or array
87
+ write_object(io, el, level + 1, options)
88
+ end
89
+ end
90
+ end
91
+
92
+ def indent(level)
93
+ ' ' * (level * 2)
94
+ end
95
+
96
+ def simple_value?(v)
97
+ v.nil? || v.is_a?(Numeric) || v.is_a?(String) || v == true || v == false
98
+ end
99
+
100
+ def format_simple(v)
101
+ case v
102
+ when String
103
+ # naive quoting: quote when contains delimiter or colon or newline
104
+ if v.empty? || v.match(/[,:\n{}\[\]]/)
105
+ '"' + v.gsub('"', '\\"') + '"'
106
+ else
107
+ v
108
+ end
109
+ when nil
110
+ 'null'
111
+ when true
112
+ 'true'
113
+ when false
114
+ 'false'
115
+ else
116
+ v.to_s
117
+ end
118
+ end
119
+
120
+ def array_uniform_hashes?(arr)
121
+ return false if arr.empty?
122
+ arr.all? { |e| e.is_a?(Hash) } && (arr.map { |h| h.keys.sort } .uniq.length == 1)
123
+ end
124
+
125
+ module_function :write_object, :write_key, :write_tabular, :write_list,
126
+ :indent, :simple_value?, :format_simple, :array_uniform_hashes?
127
+ end
128
+ end
@@ -0,0 +1,3 @@
1
+ module Toon
2
+ VERSION = '0.1.0'
3
+ end
data/lib/toon.rb ADDED
@@ -0,0 +1,28 @@
1
+ require_relative "toon/version"
2
+ require_relative "toon/encoder"
3
+ require_relative "toon/decoder"
4
+ require_relative "extensions/active_support"
5
+
6
+ module Toon
7
+ class Error < StandardError; end
8
+
9
+ def self.generate(obj, **opts)
10
+ Toon::Encoder.generate(obj, **opts)
11
+ end
12
+
13
+ def self.pretty_generate(obj, **opts)
14
+ Toon::Encoder.pretty_generate(obj, **opts)
15
+ end
16
+
17
+ def self.parse(str, **opts)
18
+ Toon::Decoder.parse(str, **opts)
19
+ end
20
+
21
+ def self.load(io, **opts)
22
+ Toon::Decoder.load(io, **opts)
23
+ end
24
+
25
+ def self.dump(obj, io = STDOUT, **opts)
26
+ Toon::Encoder.dump(obj, io, **opts)
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-toon
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Anil Yanduri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ description: 'A full-featured TOON encoder/decoder with JSON feature parity: streaming,
42
+ hooks, pretty generate, strict parsing, schema hints, CLI and ActiveSupport integration.'
43
+ email:
44
+ - anilkumaryln@gamil.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE
50
+ - README.md
51
+ - lib/extensions/active_support.rb
52
+ - lib/toon.rb
53
+ - lib/toon/decoder.rb
54
+ - lib/toon/encoder.rb
55
+ - lib/toon/version.rb
56
+ homepage: https://github.com/anilyanduri/toon
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.7.1
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.1.2
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Token-Oriented Object Notation (TOON) implementation for Ruby
79
+ test_files: []