ruby-toon 1.0.1 → 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: e32d5758d404c58449d7d8100a0d40d445c0595997620832c48f45dfd36bff9c
4
- data.tar.gz: cc942b447807a72d595e66bf76479c525545ac6ad8b2c3dd2686d04669075f89
3
+ metadata.gz: d6ebe1e066311203869e7a9c6f0d6d54e7016eddf9d8b50a47d85198ee84c458
4
+ data.tar.gz: bc1e3ab72636edc57c512c327c3802a14505ce968b197882dfd6e8093bca9069
5
5
  SHA512:
6
- metadata.gz: ca22d215cf9b70115a6e9da0d5ba29dbd1788c8d19d5c7ddbb579521bb8826604c1f3a53ad3322ea298101126cf826b31eec65265d0d55b5b4d9cc883c4ef3e4
7
- data.tar.gz: bffb33b3605dccf8a0cb011ed7c3cf1c1c27bf0a41ef8d52f2d1d09e03ea038949b35fd8fab9848c857fc6335ec97573462df8619836d7e6bf8fe78869f186de
6
+ metadata.gz: 8dc9df86a1128eb5263feb7f382398671df45d0751175ba2c1b45804fb6bb9a456e04f15de59ffa7bd5536b6dd64f429a2e74aa8f554ca42f965a451bdfd68d6
7
+ data.tar.gz: f4b9f5594635a708a9ef96a93aeba12cd395cb276e56d77a08aadcd5f0cbef7b4eee16d5db09133b052703881349c70831d0b257826f1d26bb723d9270a8c715
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
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)
2
4
 
3
5
  `toon` is a Ruby implementation of **TOON (Token-Oriented Object Notation)**
4
6
  a compact, readable, indentation-based data format designed for humans *and* machines.
@@ -2,6 +2,9 @@ module Toon
2
2
  module Extensions
3
3
  module ActiveSupport
4
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.
5
8
  def to_toon(**opts)
6
9
  payload = respond_to?(:as_json) ? as_json : self
7
10
  Toon.generate(payload, **opts)
@@ -10,16 +13,22 @@ module Toon
10
13
 
11
14
  module_function
12
15
 
16
+ # Inject +to_toon+ into Object so any model can be exported.
17
+ # @return [void]
13
18
  def install!
14
19
  return if Object.method_defined?(:to_toon)
15
20
  Object.include(ObjectMethods)
16
21
  end
17
22
 
23
+ # Install Object methods only when ActiveSupport is present.
24
+ # @return [void]
18
25
  def ensure_installed!
19
26
  return unless defined?(::ActiveSupport)
20
27
  install!
21
28
  end
22
29
 
30
+ # Attach a TracePoint that installs hooks once ActiveSupport loads.
31
+ # @return [void]
23
32
  def watch_for_active_support!
24
33
  return if defined?(@tracepoint) && @tracepoint&.enabled?
25
34
  @tracepoint = TracePoint.new(:end) do |tp|
@@ -5,14 +5,26 @@ module Toon
5
5
  module Core
6
6
  module_function
7
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.
8
12
  def hash_to_toon(target, **opts)
9
13
  Toon.generate(target, **opts)
10
14
  end
11
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.
12
20
  def array_to_toon(target, **opts)
13
21
  Toon.generate(target, **opts)
14
22
  end
15
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.
16
28
  def to_json_payload(target, *args)
17
29
  JSON.generate(target, *args)
18
30
  end
@@ -22,12 +34,18 @@ end
22
34
 
23
35
  class Hash
24
36
  unless method_defined?(:to_toon)
37
+ # Serialize the hash into TOON format.
38
+ # @param opts [Hash] encoder options.
39
+ # @return [String] TOON payload.
25
40
  def to_toon(**opts)
26
41
  Toon::Extensions::Core.hash_to_toon(self, **opts)
27
42
  end
28
43
  end
29
44
 
30
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.
31
49
  def to_json(*args)
32
50
  Toon::Extensions::Core.to_json_payload(self, *args)
33
51
  end
@@ -36,12 +54,18 @@ end
36
54
 
37
55
  class Array
38
56
  unless method_defined?(:to_toon)
57
+ # Serialize the array into TOON format.
58
+ # @param opts [Hash] encoder options.
59
+ # @return [String] TOON payload.
39
60
  def to_toon(**opts)
40
61
  Toon::Extensions::Core.array_to_toon(self, **opts)
41
62
  end
42
63
  end
43
64
 
44
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.
45
69
  def to_json(*args)
46
70
  Toon::Extensions::Core.to_json_payload(self, *args)
47
71
  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 = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
data/lib/toon.rb CHANGED
@@ -7,22 +7,43 @@ require_relative "extensions/active_support"
7
7
  module Toon
8
8
  class Error < StandardError; end
9
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.
10
14
  def self.generate(obj, **opts)
11
15
  Toon::Encoder.generate(obj, **opts)
12
16
  end
13
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.
14
22
  def self.pretty_generate(obj, **opts)
15
23
  Toon::Encoder.pretty_generate(obj, **opts)
16
24
  end
17
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.
18
30
  def self.parse(str, **opts)
19
31
  Toon::Decoder.parse(str, **opts)
20
32
  end
21
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.
22
38
  def self.load(io, **opts)
23
39
  Toon::Decoder.load(io, **opts)
24
40
  end
25
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]
26
47
  def self.dump(obj, io = STDOUT, **opts)
27
48
  Toon::Encoder.dump(obj, io, **opts)
28
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.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anil Yanduri