ruby-toon 1.0.0 β†’ 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f006a20c57bdd904663ccfd5294101e625934bc5a1df303a6f2cf471f3745df
4
- data.tar.gz: 80e3c4cd371b9eca3c47855bc79fdf67102e6c04272c69c3c1f752957c240e5d
3
+ metadata.gz: d6ebe1e066311203869e7a9c6f0d6d54e7016eddf9d8b50a47d85198ee84c458
4
+ data.tar.gz: bc1e3ab72636edc57c512c327c3802a14505ce968b197882dfd6e8093bca9069
5
5
  SHA512:
6
- metadata.gz: 6c6ef31232a2209e64815eae5f25f9bf195caee071323777eeac4a4217b45ae3937c7a8cb937d5dd60bc46f4b6c3ca0b757b0e61de58d314e67454d03f3a5b9a
7
- data.tar.gz: 1f7ee09ca1533d1ac7cb4ce3b7233f27dae0389ddfda99e9771b3087918c47e5528e858d83ddcfe69496973412a846107c80d01e6f3995a9953da8d658788870
6
+ metadata.gz: 8dc9df86a1128eb5263feb7f382398671df45d0751175ba2c1b45804fb6bb9a456e04f15de59ffa7bd5536b6dd64f429a2e74aa8f554ca42f965a451bdfd68d6
7
+ data.tar.gz: f4b9f5594635a708a9ef96a93aeba12cd395cb276e56d77a08aadcd5f0cbef7b4eee16d5db09133b052703881349c70831d0b257826f1d26bb723d9270a8c715
data/README.md CHANGED
@@ -1,22 +1,24 @@
1
- ````markdown
2
- # πŸŽ‹ Toon β€” Token-Oriented Object Notation for Ruby
1
+ # **Toon** (Token-Oriented Object Notation for Ruby)
2
+ [![Gem Version](https://badge.fury.io/rb/ruby-toon.svg)](https://badge.fury.io/rb/ruby-toon)
3
+ [![Build Status](https://github.com/anilyanduri/toon/actions/workflows/ruby.yml/badge.svg?branch=main)](https://github.com/anilyanduri/toon/actions/workflows/ruby.yml)
3
4
 
4
- `toon` is a Ruby implementation of **TOON (Token-Oriented Object Notation)** β€”
5
+ `toon` is a Ruby implementation of **TOON (Token-Oriented Object Notation)**
5
6
  a compact, readable, indentation-based data format designed for humans *and* machines.
6
7
 
7
8
  This gem provides:
8
9
 
9
- - A **TOON encoder** (Ruby β†’ TOON)
10
- - A **TOON decoder** (TOON β†’ Ruby)
10
+ - A **TOON encoder** (Ruby to TOON)
11
+ - A **TOON decoder** (TOON to Ruby)
11
12
  - A **CLI** (`bin/toon`) for converting TOON ↔ JSON
12
13
  - Optional **ActiveSupport integration** (`Object#to_toon`)
14
+ - Built-in `Hash#to_toon` / `Array#to_toon` plus lightweight `#to_json` helpers
13
15
  - Full RSpec test suite
14
16
 
15
17
  ---
16
18
 
17
- ## ✨ Features
19
+ ## Features
18
20
 
19
- ### βœ” Encode Ruby objects β†’ TOON
21
+ ### Encode Ruby objects to TOON
20
22
  ```ruby
21
23
  Toon.generate({ "name" => "Alice", "age" => 30 })
22
24
  ````
@@ -28,7 +30,7 @@ name:Alice
28
30
  age:30
29
31
  ```
30
32
 
31
- ### βœ” Decode TOON β†’ Ruby
33
+ ### Decode TOON to Ruby
32
34
 
33
35
  ```ruby
34
36
  Toon.parse("name:Alice\nage:30")
@@ -42,7 +44,7 @@ Returns:
42
44
 
43
45
  ---
44
46
 
45
- ## 🧩 Arrays
47
+ ## Arrays
46
48
 
47
49
  ### Nested arrays (encoder output)
48
50
 
@@ -67,7 +69,7 @@ Both decode correctly.
67
69
 
68
70
  ---
69
71
 
70
- ## πŸ“Š Tabular Arrays
72
+ ## Tabular Arrays
71
73
 
72
74
  ### Nested tabular (encoder output)
73
75
 
@@ -78,7 +80,7 @@ users:
78
80
  2,B
79
81
  ```
80
82
 
81
- β†’ numeric fields parsed (`id` becomes integer)
83
+ numeric fields parsed (`id` becomes integer)
82
84
 
83
85
  ### Flat tabular (user input)
84
86
 
@@ -88,27 +90,49 @@ users[2]{id,name}:
88
90
  2,Bob
89
91
  ```
90
92
 
91
- β†’ fields remain **strings**
93
+ fields remain **strings**
92
94
 
93
95
  ---
94
96
 
95
- ## βš™οΈ ActiveSupport Integration
97
+ ## ActiveSupport Integration
96
98
 
97
- If ActiveSupport is installed:
99
+ If ActiveSupport (and by extension Rails/Active Record) is installedβ€”regardless of whether it loads before or after `toon`β€”every object gains `#to_toon`.
98
100
 
99
101
  ```ruby
102
+ require "toon"
100
103
  require "active_support"
104
+
105
+ class User < ApplicationRecord; end
106
+
107
+ User.first.to_toon
108
+ # => "id:1\nname:Alice\n..."
109
+ ```
110
+
111
+ - Automatically hooks in as soon as ActiveSupport finishes loading (thanks to a TracePoint watcher)
112
+ - Falls back to `#as_json` when present, so Active Record / ActiveModel instances serialize their attributes instead of opaque object IDs
113
+
114
+ ## Core Extensions
115
+
116
+ `toon` now provides handy helpers even without ActiveSupport:
117
+
118
+ ```ruby
101
119
  require "toon"
102
120
 
103
- {a: 1}.to_toon
104
- # => "a:1\n"
121
+ {foo: "bar"}.to_toon
122
+ # => "foo:bar"
123
+
124
+ [1, 2, 3].to_toon
125
+ # => "[3]:\n 1\n 2\n 3\n"
126
+
127
+ {foo: "bar"}.to_json
128
+ # => "{\"foo\":\"bar\"}"
105
129
  ```
106
130
 
107
- Adds `Object#to_toon` for convenience.
131
+ Both `Hash` and `Array` gain `#to_toon` and `#to_json` implementations so you can round-trip data between TOON and JSON with a single method call.
108
132
 
109
133
  ---
110
134
 
111
- ## πŸš€ Installation
135
+ ## Installation
112
136
 
113
137
  Gem coming soon. For now:
114
138
 
@@ -126,15 +150,15 @@ require_relative "lib/toon"
126
150
 
127
151
  ---
128
152
 
129
- ## 🧰 CLI Usage
153
+ ## CLI Usage
130
154
 
131
- ### Encode JSON β†’ TOON
155
+ ### Encode JSON to TOON
132
156
 
133
157
  ```bash
134
158
  echo '{"name":"Alice","age":30}' | bin/toon --encode
135
159
  ```
136
160
 
137
- ### Decode TOON β†’ JSON
161
+ ### Decode TOON to JSON
138
162
 
139
163
  ```bash
140
164
  echo "name:Alice\nage:30" | bin/toon --decode
@@ -148,7 +172,7 @@ bin/toon --encode < input.json
148
172
 
149
173
  ---
150
174
 
151
- ## πŸ§ͺ Running Tests
175
+ ## Running Tests
152
176
 
153
177
  Ensure CLI is executable:
154
178
 
@@ -171,7 +195,7 @@ Tests include:
171
195
 
172
196
  ---
173
197
 
174
- ## πŸ“š Supported TOON Grammar (Current)
198
+ ## Supported TOON Grammar (Current)
175
199
 
176
200
  ### Key-value
177
201
 
@@ -225,7 +249,7 @@ users[2]{id,name}:
225
249
 
226
250
  ---
227
251
 
228
- ## ⚠️ Error Handling
252
+ ## Error Handling
229
253
 
230
254
  Malformed input (e.g., missing indentation):
231
255
 
@@ -237,7 +261,7 @@ Decoder stops with a friendly `Toon::Error`.
237
261
 
238
262
  ---
239
263
 
240
- ## πŸ—ΊοΈ Roadmap
264
+ ## Roadmap
241
265
 
242
266
  * Multiline values
243
267
  * Quoted strings
@@ -1,12 +1,50 @@
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)
1
+ module Toon
2
+ module Extensions
3
+ module ActiveSupport
4
+ module ObjectMethods
5
+ # Serialize the object into TOON, preferring +as_json+ when available.
6
+ # @param opts [Hash] encoder options passed to {Toon.generate}.
7
+ # @return [String] TOON payload.
8
+ def to_toon(**opts)
9
+ payload = respond_to?(:as_json) ? as_json : self
10
+ Toon.generate(payload, **opts)
11
+ end
12
+ end
13
+
14
+ module_function
15
+
16
+ # Inject +to_toon+ into Object so any model can be exported.
17
+ # @return [void]
18
+ def install!
19
+ return if Object.method_defined?(:to_toon)
20
+ Object.include(ObjectMethods)
21
+ end
22
+
23
+ # Install Object methods only when ActiveSupport is present.
24
+ # @return [void]
25
+ def ensure_installed!
26
+ return unless defined?(::ActiveSupport)
27
+ install!
28
+ end
29
+
30
+ # Attach a TracePoint that installs hooks once ActiveSupport loads.
31
+ # @return [void]
32
+ def watch_for_active_support!
33
+ return if defined?(@tracepoint) && @tracepoint&.enabled?
34
+ @tracepoint = TracePoint.new(:end) do |tp|
35
+ next unless tp.self.is_a?(Module)
36
+ next unless tp.self.name == "ActiveSupport"
37
+ ensure_installed!
38
+ @tracepoint.disable
39
+ end
40
+ @tracepoint.enable
41
+ end
8
42
  end
9
43
  end
10
- rescue LoadError
11
- # no activesupport available
44
+ end
45
+
46
+ if defined?(::ActiveSupport)
47
+ Toon::Extensions::ActiveSupport.ensure_installed!
48
+ else
49
+ Toon::Extensions::ActiveSupport.watch_for_active_support!
12
50
  end
@@ -0,0 +1,73 @@
1
+ require 'json'
2
+
3
+ module Toon
4
+ module Extensions
5
+ module Core
6
+ module_function
7
+
8
+ # Serialize a Hash-like object into TOON using the global encoder.
9
+ # @param target [Hash] structure to encode.
10
+ # @param opts [Hash] options passed through to {Toon.generate}.
11
+ # @return [String] TOON string.
12
+ def hash_to_toon(target, **opts)
13
+ Toon.generate(target, **opts)
14
+ end
15
+
16
+ # Serialize an Array into TOON by delegating to {Toon.generate}.
17
+ # @param target [Array] structure to encode.
18
+ # @param opts [Hash] options passed through to the encoder.
19
+ # @return [String] TOON string.
20
+ def array_to_toon(target, **opts)
21
+ Toon.generate(target, **opts)
22
+ end
23
+
24
+ # Convert the object to JSON using the bundled JSON gem.
25
+ # @param target [Object] structure to encode.
26
+ # @param args [Array] options forwarded to {JSON.generate}.
27
+ # @return [String] JSON payload.
28
+ def to_json_payload(target, *args)
29
+ JSON.generate(target, *args)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ class Hash
36
+ unless method_defined?(:to_toon)
37
+ # Serialize the hash into TOON format.
38
+ # @param opts [Hash] encoder options.
39
+ # @return [String] TOON payload.
40
+ def to_toon(**opts)
41
+ Toon::Extensions::Core.hash_to_toon(self, **opts)
42
+ end
43
+ end
44
+
45
+ unless method_defined?(:to_json)
46
+ # Serialize the hash into JSON via the core extension helper.
47
+ # @param args [Array] JSON serialization options.
48
+ # @return [String] JSON payload.
49
+ def to_json(*args)
50
+ Toon::Extensions::Core.to_json_payload(self, *args)
51
+ end
52
+ end
53
+ end
54
+
55
+ class Array
56
+ unless method_defined?(:to_toon)
57
+ # Serialize the array into TOON format.
58
+ # @param opts [Hash] encoder options.
59
+ # @return [String] TOON payload.
60
+ def to_toon(**opts)
61
+ Toon::Extensions::Core.array_to_toon(self, **opts)
62
+ end
63
+ end
64
+
65
+ unless method_defined?(:to_json)
66
+ # Serialize the array into JSON via the core extension helper.
67
+ # @param args [Array] JSON serialization options.
68
+ # @return [String] JSON payload.
69
+ def to_json(*args)
70
+ Toon::Extensions::Core.to_json_payload(self, *args)
71
+ end
72
+ end
73
+ end
data/lib/toon/decoder.rb CHANGED
@@ -2,181 +2,199 @@ module Toon
2
2
  module Decoder
3
3
  module_function
4
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
5
+ # Parse a TOON string and produce the corresponding Ruby structure.
6
+ # @param str [String] TOON payload.
7
+ # @param opts [Hash] reserved for future parser options.
8
+ # @return [Object] reconstructed Ruby data.
9
+ def parse(str, **opts)
10
+ lines = str.gsub("\r\n", "\n").split("\n")
11
+ parse_lines(lines)
12
+ end
13
13
 
14
- def parse_lines(lines)
15
- root = {}
16
- stack = [ { indent: -1, obj: root, parent: nil, key: nil } ]
14
+ # Read all data from an IO-like object and parse it as TOON.
15
+ # @param io [#read] source stream.
16
+ # @param opts [Hash] reserved for future parser options.
17
+ # @return [Object] reconstructed Ruby data.
18
+ def load(io, **opts)
19
+ parse(io.read, **opts)
20
+ end
17
21
 
18
- i = 0
19
- while i < lines.length
20
- raw = lines[i].rstrip
21
- i += 1
22
+ # Core parser that works on an array of sanitized TOON lines.
23
+ # @param lines [Array<String>] TOON lines without newlines.
24
+ # @return [Hash] root object constructed from the input.
25
+ def parse_lines(lines)
26
+ root = {}
27
+ stack = [ { indent: -1, obj: root, parent: nil, key: nil } ]
22
28
 
23
- next if raw.strip.empty? || raw.strip.start_with?('#')
29
+ i = 0
30
+ while i < lines.length
31
+ raw = lines[i].rstrip
32
+ i += 1
24
33
 
25
- indent = leading_spaces(raw)
26
- content = raw.strip
34
+ next if raw.strip.empty? || raw.strip.start_with?('#')
27
35
 
28
- # Fix indentation
29
- while indent <= stack.last[:indent]
30
- stack.pop
31
- end
36
+ indent = leading_spaces(raw)
37
+ content = raw.strip
32
38
 
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)]
39
+ # Fix indentation
40
+ while indent <= stack.last[:indent]
41
+ stack.pop
51
42
  end
52
43
 
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)
44
+ current = stack.last
45
+ parent_obj = current[:obj]
46
+
47
+ # ============================================================
48
+ # FLAT TABULAR ARRAY: users[2]{id,name}:
49
+ # ============================================================
50
+ if m = content.match(/^(\w+)\[(\d+)\]\{([^}]*)\}:$/)
51
+ key = m[1]
52
+ count = m[2].to_i
53
+ fields = m[3].split(",").map(&:strip)
54
+
55
+ rows = []
56
+ count.times do
57
+ row = lines[i]&.strip
58
+ i += 1
59
+ next unless row
60
+ values = split_row(row.strip)
61
+ rows << Hash[fields.zip(values)]
62
+ end
63
+
64
+ parent_obj[key] = rows
65
+ next
70
66
  end
71
67
 
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)]
68
+ # ============================================================
69
+ # FLAT PRIMITIVE ARRAY: colors[3]:
70
+ # ============================================================
71
+ if m = content.match(/^(\w+)\[(\d+)\]:$/)
72
+ key = m[1]
73
+ count = m[2].to_i
74
+
75
+ values = []
76
+ count.times do
77
+ row = lines[i]&.strip
78
+ i += 1
79
+ next unless row
80
+ values << parse_scalar(row)
81
+ end
82
+
83
+ parent_obj[key] = values
84
+ next
109
85
  end
110
86
 
111
- parent[key] = rows
112
- stack.pop
113
- next
114
- end
87
+ # ============================================================
88
+ # NESTED OBJECT KEY: key:
89
+ # ============================================================
90
+ if m = content.match(/^(\w+):$/)
91
+ key = m[1]
92
+ new_obj = {}
115
93
 
116
- # ============================================================
117
- # NESTED PRIMITIVE ARRAY:
118
- # colors:
119
- # [3]:
120
- # ============================================================
121
- if m = content.match(/^\[(\d+)\]:$/)
122
- count = m[1].to_i
94
+ parent_obj[key] = new_obj
95
+ stack << { indent: indent, obj: new_obj, parent: parent_obj, key: key }
96
+ next
97
+ end
123
98
 
124
- if key.nil?
125
- raise Toon::Error, "Malformed TOON: array header '#{content}' must be under a key (e.g., 'colors:')"
99
+ # Refresh frame for nested array parsing
100
+ frame = stack.last
101
+ parent = frame[:parent]
102
+ key = frame[:key]
103
+
104
+ # ============================================================
105
+ # NESTED TABULAR ARRAY:
106
+ # users:
107
+ # [2]{id,name}:
108
+ # ============================================================
109
+ if m = content.match(/^\[(\d+)\]\{([^}]*)\}:$/)
110
+ count = m[1].to_i
111
+ fields = m[2].split(',').map(&:strip)
112
+
113
+ rows = []
114
+ count.times do
115
+ row = lines[i]&.strip
116
+ i += 1
117
+ next unless row
118
+ values = split_row(row).map { |v| parse_scalar(v) }
119
+ rows << Hash[fields.zip(values)]
120
+ end
121
+
122
+ parent[key] = rows
123
+ stack.pop
124
+ next
126
125
  end
127
126
 
128
- values = []
129
- count.times do
130
- row = lines[i]&.strip
131
- i += 1
132
- next unless row
133
- values << parse_scalar(row)
127
+ # ============================================================
128
+ # NESTED PRIMITIVE ARRAY:
129
+ # colors:
130
+ # [3]:
131
+ # ============================================================
132
+ if m = content.match(/^\[(\d+)\]:$/)
133
+ count = m[1].to_i
134
+
135
+ if key.nil?
136
+ raise Toon::Error, "Malformed TOON: array header '#{content}' must be under a key (e.g., 'colors:')"
137
+ end
138
+
139
+ values = []
140
+ count.times do
141
+ row = lines[i]&.strip
142
+ i += 1
143
+ next unless row
144
+ values << parse_scalar(row)
145
+ end
146
+
147
+ parent[key] = values
148
+ stack.pop
149
+ next
134
150
  end
135
151
 
136
- parent[key] = values
137
- stack.pop
138
- next
152
+ # ============================================================
153
+ # SIMPLE KEY: VALUE
154
+ # ============================================================
155
+ if m = content.match(/^(\w+):\s*(.*)$/)
156
+ k = m[1]
157
+ v = parse_scalar(m[2])
158
+ parent_obj[k] = v
159
+ next
160
+ end
139
161
  end
140
162
 
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
163
+ root
150
164
  end
151
165
 
152
- root
153
- end
154
-
155
-
156
-
157
- def leading_spaces(line)
158
- line[/^ */].length
159
- end
166
+ # Count the indentation depth (in spaces) at the beginning of +line+.
167
+ # @param line [String] line from the TOON payload.
168
+ # @return [Integer] number of leading spaces.
169
+ def leading_spaces(line)
170
+ line[/^ */].length
171
+ end
160
172
 
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
173
+ # Convert a scalar token into the appropriate Ruby object.
174
+ # @param str [String] textual token.
175
+ # @return [Object] decoded scalar (String, Numeric, true/false, nil).
176
+ def parse_scalar(str)
177
+ # strip quotes
178
+ if str == 'null' then nil
179
+ elsif str == 'true' then true
180
+ elsif str == 'false' then false
181
+ elsif str.match?(/^".*"$/)
182
+ str[1..-2].gsub('\\"','"')
183
+ elsif str =~ /^-?\d+$/
184
+ str.to_i
185
+ elsif str =~ /^-?\d+\.\d+$/
186
+ str.to_f
187
+ else
188
+ str
189
+ end
174
190
  end
175
- end
176
191
 
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
192
+ # Split a comma-delimited row while preserving quoted delimiters.
193
+ # @param row [String] raw row contents.
194
+ # @return [Array<String>] tokenized column values.
195
+ def split_row(row)
196
+ # simplistic split on comma that ignores quoted commas - naive
197
+ row.scan(/(?:\"([^\"]*)\"|([^,]+))(?:,|$)/).map { |m| (m[0] || m[1]).to_s.strip }
198
+ end
181
199
  end
182
200
  end
data/lib/toon/encoder.rb CHANGED
@@ -10,117 +10,161 @@ module Toon
10
10
  pretty: false
11
11
  }
12
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
13
+ # Generate a TOON string for +obj+ with the desired encoder options.
14
+ # @param obj [Object] structure to serialize.
15
+ # @param opts [Hash] overrides for {DEFAULT_OPTIONS}.
16
+ # @return [String] TOON payload.
17
+ def generate(obj, **opts)
18
+ options = DEFAULT_OPTIONS.merge(opts)
19
+ io = StringIO.new
20
+ write_object(io, obj, 0, options)
21
+ io.string
22
+ end
19
23
 
20
- def pretty_generate(obj, **opts)
21
- generate(obj, **opts.merge(pretty: true))
22
- end
24
+ # Generate a human-friendly TOON payload irrespective of caller options.
25
+ # @param obj [Object] structure to serialize.
26
+ # @param opts [Hash] encoder overrides.
27
+ # @return [String] prettified TOON payload.
28
+ def pretty_generate(obj, **opts)
29
+ generate(obj, **opts.merge(pretty: true))
30
+ end
23
31
 
24
- def dump(obj, io = STDOUT, **opts)
25
- str = generate(obj, **opts)
26
- io.write(str)
27
- nil
28
- end
32
+ # Stream the serialized payload directly to an IO target.
33
+ # @param obj [Object] data to encode.
34
+ # @param io [#write] destination stream (defaults to STDOUT).
35
+ # @param opts [Hash] encoder overrides.
36
+ # @return [nil]
37
+ def dump(obj, io = STDOUT, **opts)
38
+ str = generate(obj, **opts)
39
+ io.write(str)
40
+ nil
41
+ end
29
42
 
30
- private
43
+ private
31
44
 
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")
45
+ # Dispatch encoding logic for hashes, arrays, or scalar values.
46
+ # @param io [#write] accumulating stream.
47
+ # @param obj [Object] value to encode.
48
+ # @param level [Integer] depth in the output tree.
49
+ # @param options [Hash] encoder configuration.
50
+ def write_object(io, obj, level, options)
51
+ case obj
52
+ when Hash
53
+ obj.each do |k, v|
54
+ write_key(io, k, level)
55
+ if simple_value?(v)
56
+ io.write(format_simple(v))
57
+ io.write("\n")
58
+ else
59
+ io.write("\n")
60
+ write_object(io, v, level + 1, options)
61
+ end
62
+ end
63
+ when Array
64
+ # Try to detect uniform objects for tabular style
65
+ if options[:compact_arrays] && array_uniform_hashes?(obj)
66
+ write_tabular(io, obj, level, options)
40
67
  else
41
- io.write("\n")
42
- write_object(io, v, level + 1, options)
68
+ write_list(io, obj, level, options)
43
69
  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
70
  else
50
- write_list(io, obj, level, options)
71
+ # scalar
72
+ io.write(indent(level))
73
+ io.write(format_simple(obj))
74
+ io.write("\n")
51
75
  end
52
- else
53
- # scalar
54
- io.write(indent(level))
55
- io.write(format_simple(obj))
56
- io.write("\n")
57
76
  end
58
- end
59
-
60
- def write_key(io, key, level)
61
- io.write(indent(level))
62
- io.write("#{key}:")
63
- end
64
77
 
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")
78
+ # Emit a hash key label aligned to the requested indentation depth.
79
+ # @param io [#write] accumulating stream.
80
+ # @param key [String, Symbol] key to render.
81
+ # @param level [Integer] indentation level.
82
+ def write_key(io, key, level)
83
+ io.write(indent(level))
84
+ io.write("#{key}:")
74
85
  end
75
- end
76
86
 
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))
87
+ # Render an array of hashes using the compact tabular notation.
88
+ # @param io [#write] accumulating stream.
89
+ # @param arr [Array<Hash>] rows to encode.
90
+ # @param level [Integer] indentation level.
91
+ # @param options [Hash] encoder configuration.
92
+ def write_tabular(io, arr, level, options)
93
+ keys = (arr.map(&:keys).reduce(:|) || []).uniq
94
+ io.write(indent(level))
95
+ io.write("[#{arr.length}]{#{keys.join(',')}}:\n")
96
+ arr.each do |row|
97
+ io.write(indent(level + 1))
98
+ vals = keys.map { |k| format_simple(row[k]) }
99
+ io.write(vals.join(options[:delimiter]))
84
100
  io.write("\n")
85
- else
86
- # nested object or array
87
- write_object(io, el, level + 1, options)
88
101
  end
89
102
  end
90
- end
91
103
 
92
- def indent(level)
93
- ' ' * (level * 2)
94
- end
104
+ # Render a heterogeneous array in the multi-line list form.
105
+ # @param io [#write] accumulating stream.
106
+ # @param arr [Array] values to encode.
107
+ # @param level [Integer] indentation level.
108
+ # @param options [Hash] encoder configuration.
109
+ def write_list(io, arr, level, options)
110
+ io.write(indent(level))
111
+ io.write("[#{arr.length}]:\n")
112
+ arr.each do |el|
113
+ io.write(indent(level + 1))
114
+ if simple_value?(el)
115
+ io.write(format_simple(el))
116
+ io.write("\n")
117
+ else
118
+ # nested object or array
119
+ write_object(io, el, level + 1, options)
120
+ end
121
+ end
122
+ end
95
123
 
96
- def simple_value?(v)
97
- v.nil? || v.is_a?(Numeric) || v.is_a?(String) || v == true || v == false
98
- end
124
+ # Produce indentation padding for the supplied level.
125
+ # @param level [Integer] nesting depth.
126
+ # @return [String] spaces for indent.
127
+ def indent(level)
128
+ ' ' * (level * 2)
129
+ end
130
+
131
+ # Identify whether +v+ is a scalar that can be inlined.
132
+ # @param v [Object] value to test.
133
+ # @return [Boolean] true if +v+ is nil, numeric, boolean, or string.
134
+ def simple_value?(v)
135
+ v.nil? || v.is_a?(Numeric) || v.is_a?(String) || v == true || v == false
136
+ end
99
137
 
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('"', '\\"') + '"'
138
+ # Convert a scalar value into its TOON string representation.
139
+ # @param v [Object] scalar value.
140
+ # @return [String] formatted representation.
141
+ def format_simple(v)
142
+ case v
143
+ when String
144
+ # naive quoting: quote when contains delimiter or colon or newline
145
+ if v.empty? || v.match(/[,:\n{}\[\]]/)
146
+ '"' + v.gsub('"', '\\"') + '"'
147
+ else
148
+ v
149
+ end
150
+ when nil
151
+ 'null'
152
+ when true
153
+ 'true'
154
+ when false
155
+ 'false'
106
156
  else
107
- v
157
+ v.to_s
108
158
  end
109
- when nil
110
- 'null'
111
- when true
112
- 'true'
113
- when false
114
- 'false'
115
- else
116
- v.to_s
117
159
  end
118
- end
119
160
 
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
161
+ # Check whether +arr+ contains only hashes sharing the same keys.
162
+ # @param arr [Array] array to test.
163
+ # @return [Boolean] true when the compact tabular format applies.
164
+ def array_uniform_hashes?(arr)
165
+ return false if arr.empty?
166
+ arr.all? { |e| e.is_a?(Hash) } && (arr.map { |h| h.keys.sort }.uniq.length == 1)
167
+ end
124
168
 
125
169
  module_function :write_object, :write_key, :write_tabular, :write_list,
126
170
  :indent, :simple_value?, :format_simple, :array_uniform_hashes?
data/lib/toon/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Toon
2
- VERSION = '0.1.0'
2
+ VERSION = '1.0.2'
3
3
  end
data/lib/toon.rb CHANGED
@@ -1,27 +1,49 @@
1
1
  require_relative "toon/version"
2
2
  require_relative "toon/encoder"
3
3
  require_relative "toon/decoder"
4
+ require_relative "extensions/core"
4
5
  require_relative "extensions/active_support"
5
6
 
6
7
  module Toon
7
8
  class Error < StandardError; end
8
9
 
10
+ # Generate a TOON-formatted String from any serializable Ruby object.
11
+ # @param obj [Object] the object to encode.
12
+ # @param opts [Hash] encoder options forwarded to `Toon::Encoder`.
13
+ # @return [String] the TOON payload.
9
14
  def self.generate(obj, **opts)
10
15
  Toon::Encoder.generate(obj, **opts)
11
16
  end
12
17
 
18
+ # Generate a human-friendly TOON String with extra spacing and indentation.
19
+ # @param obj [Object] the object to encode.
20
+ # @param opts [Hash] encoder options forwarded to `Toon::Encoder`.
21
+ # @return [String] the prettified TOON payload.
13
22
  def self.pretty_generate(obj, **opts)
14
23
  Toon::Encoder.pretty_generate(obj, **opts)
15
24
  end
16
25
 
26
+ # Parse a TOON String and reconstruct the Ruby data structure.
27
+ # @param str [String] the TOON representation.
28
+ # @param opts [Hash] decoder options forwarded to `Toon::Decoder`.
29
+ # @return [Object] the decoded Ruby structure.
17
30
  def self.parse(str, **opts)
18
31
  Toon::Decoder.parse(str, **opts)
19
32
  end
20
33
 
34
+ # Read a TOON payload from any IO-like object and parse it.
35
+ # @param io [#read] an IO that responds to `#read`.
36
+ # @param opts [Hash] decoder options forwarded to `Toon::Decoder`.
37
+ # @return [Object] the decoded Ruby structure.
21
38
  def self.load(io, **opts)
22
39
  Toon::Decoder.load(io, **opts)
23
40
  end
24
41
 
42
+ # Stream a TOON payload for +obj+ into the provided IO target.
43
+ # @param obj [Object] the object to encode.
44
+ # @param io [#write] where the payload should be written.
45
+ # @param opts [Hash] encoder options forwarded to `Toon::Encoder`.
46
+ # @return [nil]
25
47
  def self.dump(obj, io = STDOUT, **opts)
26
48
  Toon::Encoder.dump(obj, io, **opts)
27
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-toon
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anil Yanduri
@@ -49,6 +49,7 @@ files:
49
49
  - LICENSE
50
50
  - README.md
51
51
  - lib/extensions/active_support.rb
52
+ - lib/extensions/core.rb
52
53
  - lib/toon.rb
53
54
  - lib/toon/decoder.rb
54
55
  - lib/toon/encoder.rb