toon-fu 4.1.4 → 4.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a5a734c469d08f497c36883702683c8a1c7edc3245df9f886d5f6159ff26db1
4
- data.tar.gz: 784178fff8819a4895e5252b8e356ffaddaceca1ff10d9d6067a73984d5ef8cc
3
+ metadata.gz: c7f87e2144c7adb60f949127ea36ed9e26b84fb1ba5e4cd6893d9fc46f3e859e
4
+ data.tar.gz: 84c74750d67f681645a7cd0e84b46675919f3e0549cc4e168306510b1f8b45bf
5
5
  SHA512:
6
- metadata.gz: 4acf57be096b69fa1c675a97efb59fe63db7b66234e649305f5cb1dafa319465fa3ffb235d2aa252dd714861476720a910b7859e6916dc6ba45584b1d75a886c
7
- data.tar.gz: 3ba5aa86ccfae68c53da20cfaa66a31d3ddee6b5e57af7c72f3ef884d36be45fe485b234973cd6cef2b31d1751d482d1f36b20a9d5eff746f02ff486ccc997e3
6
+ metadata.gz: a78c3f872ff747c74af86f91a5e945b8dcf1b2eb5d10ea9694ad2a0e3efccc938fe7652a5cfda36713163f0f39d965dbe75f1e77698bdea3aea124e81bb1b194
7
+ data.tar.gz: 1b6fa5a87123368f9a7e92ee1fb1f91fce1950d583967acf007542b6ea10168c8b67c282cb87e96009841a039b657a23c77c11d4e47535d192bde41f9e82aaad
data/README.md CHANGED
@@ -138,7 +138,7 @@ ToonFu.decode("tags[3]: a,b")
138
138
 
139
139
  ### Options
140
140
 
141
- - `strict:` — `true` (default) checks what the spec requires: declared lengths, row widths, duplicate keys, indentation. Pass `false` to read a document that bends those rules — a declared length is ignored, a duplicate key takes its last value.
141
+ - `strict:` — `true` (default) checks what the spec requires: declared lengths, row widths, duplicate keys, indentation. Pass `false` to read a document that bends those rules — a declared length is ignored, a duplicate key takes its last value, a stray over-indented line is skipped unless it is a bare token, which raises in either mode.
142
142
  - `indent_size:` — spaces per nesting level, default `2`.
143
143
 
144
144
  ```ruby
@@ -166,17 +166,17 @@ b08ea2d feat: ship a toon executable
166
166
 
167
167
  ## Compared with other Ruby TOON gems
168
168
 
169
- The only one that passes every spec fixture: toon-fu passes all 179 encode and all 359 decode fixtures. The table compares encoding — the 154 encode fixtures that use default options, which every gem can run; none of the others reads TOON back.
169
+ The only one that passes every spec fixture: toon-fu passes all 160 encode and all 385 decode fixtures. The table compares encoding — the 138 encode fixtures that use default options, which every gem can run; none of the others reads TOON back.
170
170
 
171
171
  | Gem | Spec fixtures passed | Speed vs toon-fu |
172
172
  |---|---:|---:|
173
- | **toon-fu** | **154 / 154** | **1.00×** |
174
- | sorbet-toon 0.1.0 | 119 / 154 | 0.59× |
175
- | toon-ruby 0.1.1 | 117 / 154 | 0.62× |
176
- | toon_my_json 0.1.0 | 57 / 154 | 1.68× |
177
- | toon-format 0.1.2 | 45 / 154 | 1.18× |
173
+ | **toon-fu** | **138 / 138** | **1.00×** |
174
+ | sorbet-toon 0.1.0 | 104 / 138 | 0.63× |
175
+ | toon-ruby 0.1.1 | 100 / 138 | 0.62× |
176
+ | toon_my_json 0.1.0 | 48 / 138 | 1.62× |
177
+ | toon-format 0.1.2 | 40 / 138 | 1.31× |
178
178
 
179
- The Ruby TOON encoders with more than 10,000 downloads, measured by [`benchmark/run.rb`](benchmark/run.rb) on Ruby 3.4.10 (2026-09-24). **Spec fixtures** are the spec's own encode fixtures that use default options. **Speed** is the geometric mean of encodes per second over five workloads — tables of 100 and 1000 rows, nested objects, a list of mixed objects, strings needing quotes — relative to toon-fu.
179
+ The Ruby TOON encoders with more than 10,000 downloads, measured by [`benchmark/run.rb`](benchmark/run.rb) on Ruby 3.4.11 (2026-10-07). **Spec fixtures** are the spec's own encode fixtures that use default options. **Speed** is the geometric mean of encodes per second over five workloads — tables of 100 and 1000 rows, nested objects, a list of mixed objects, strings needing quotes — relative to toon-fu.
180
180
 
181
181
  What falls through the gaps:
182
182
 
@@ -203,7 +203,7 @@ The gem version tracks the TOON specification it implements:
203
203
  - `MAJOR.MINOR` is the spec version: `X.Y.Z` speaks TOON `X.Y`.
204
204
  - `PATCH` is the gem's own: fixes and improvements within the same dialect.
205
205
 
206
- This release speaks `toon-spec: 4.1`.
206
+ This release speaks `toon-spec: 4.3`.
207
207
 
208
208
  Pin the spec line with `gem "toon-fu", "~> X.Y.0"`: it takes our fixes and keeps you on the dialect you speak. The gem badge above shows the current release; the spec-drift badge turns red when a newer spec is released and toon-fu has not caught up yet.
209
209
 
@@ -5,8 +5,11 @@ module ToonFu
5
5
  class Decoder
6
6
  # @param strict [Boolean] whether to enforce the spec's strict-mode
7
7
  # checks; with +false+ duplicate keys resolve last-write-wins, leading
8
- # spaces that are not a multiple of indent_size are accepted, and a
9
- # tab in indentation is accepted and counts as one level
8
+ # spaces that are not a multiple of indent_size are accepted, a tab in
9
+ # indentation is accepted and counts as one level, a line of spaces and
10
+ # tabs is blank, a scope's first line sets the scope's depth, an
11
+ # over-indented line is skipped unless it is a bare token, and a
12
+ # malformed or misplaced header is read as a key-value line
10
13
  # @param indent_size [Integer] spaces per nesting level
11
14
  # @raise [ArgumentError] when strict is not a boolean or indent_size is
12
15
  # not a positive Integer
@@ -43,9 +46,10 @@ module ToonFu
43
46
  # key without a colon; for a bare token line inside a scope; for
44
47
  # content after a completed root form; for nesting beyond the depth
45
48
  # limit; and, when strict, for a declared length or row width that
46
- # does not match, a malformed header, a duplicate key, a blank line
47
- # inside a header's scope, and indentation that is not a multiple of
48
- # indent_size
49
+ # does not match, a malformed or misplaced header, a duplicate key, a
50
+ # blank line inside a header's scope, a tab in indentation, indentation
51
+ # that is not a multiple of indent_size, a depth jump, and an
52
+ # over-indented line, an indented first line included
49
53
  def decode(text)
50
54
  Reader.new(@strict, @indent_size).read(utf8(text))
51
55
  end
@@ -37,16 +37,21 @@ module ToonFu
37
37
  end
38
38
 
39
39
  def split(entry)
40
+ entry = Tokens.trim(entry)
41
+ return [nil, nil] if entry.empty?
42
+
40
43
  brace = Tokens.brace_index(entry)
41
- return [Tokens.key(entry), nil] if brace.nil?
44
+ return [Token.key(entry), nil] if brace.nil?
42
45
 
43
46
  name = entry[0...brace]
47
+ return [nil, nil] if name.empty? || name.match?(Tokens::TRAILING_WHITESPACE)
48
+
44
49
  group = entry[brace..]
45
50
  closing = Tokens.closing_brace(group)
46
51
  return [nil, nil] unless closing == group.length - 1
47
52
 
48
53
  nested = self.class.parse(group[1...closing], @delimiter)
49
- nested.nil? ? [nil, nil] : [Tokens.key(name), nested]
54
+ nested.nil? ? [nil, nil] : [Token.key(name), nested]
50
55
  end
51
56
  end
52
57
  end
@@ -9,7 +9,7 @@ module ToonFu
9
9
 
10
10
  def self.parse(content)
11
11
  bracket = Tokens.bracket_index(content)
12
- return nil if bracket.nil?
12
+ return nil if bracket.nil? || Tokens.colon_index(content).nil?
13
13
 
14
14
  new(content, bracket).parse
15
15
  end
@@ -20,9 +20,9 @@ module ToonFu
20
20
  @malformed = false
21
21
  end
22
22
 
23
- # A line carrying a bracket segment is either a header, or an attempt at
24
- # one that §6 makes a strict-mode error; only a line without one falls
25
- # through to the key-value class untouched.
23
+ # A line whose first unquoted "[" precedes its first unquoted colon is an
24
+ # array-header line; one that fails the §6 grammar is malformed, a
25
+ # strict-mode error (§5.2).
26
26
  def malformed?
27
27
  @malformed
28
28
  end
@@ -30,7 +30,7 @@ module ToonFu
30
30
  def parse
31
31
  @key = @bracket.zero? ? nil : @content[0...@bracket]
32
32
  return nil unless @key.nil? || Tokens.whole_token?(@key)
33
- return broken if @key&.match?(/\s\z/)
33
+ return broken if @key&.match?(Tokens::TRAILING_WHITESPACE)
34
34
  return broken unless (closing = @content.index("]", @bracket))
35
35
 
36
36
  return broken unless segment(@content[(@bracket + 1)...closing])
data/lib/toon_fu/lines.rb CHANGED
@@ -29,8 +29,8 @@ module ToonFu
29
29
  BOM = "\u{feff}"
30
30
  COMMENT = /\A *#/
31
31
  TRAILING_SPACES = / +\z/
32
- BLANK = /\A\s*\z/
33
32
  INDENTATION = /\A[ \t]*/
33
+ BLANK_WITH_TABS = /\A[ \t]*\z/
34
34
 
35
35
  def initialize(text, strict, indent_size)
36
36
  @strict = strict
@@ -69,7 +69,7 @@ module ToonFu
69
69
  line = raw.delete_suffix("\r").sub(TRAILING_SPACES, "")
70
70
  next if COMMENT.match?(line)
71
71
 
72
- if BLANK.match?(line)
72
+ if blank?(line)
73
73
  blank = true
74
74
  next
75
75
  end
@@ -80,6 +80,10 @@ module ToonFu
80
80
  end
81
81
  end
82
82
 
83
+ def blank?(line)
84
+ @strict ? line.empty? : BLANK_WITH_TABS.match?(line)
85
+ end
86
+
83
87
  def build(line, number, blank)
84
88
  Line.new(line.sub(INDENTATION, ""), depth(line, number), number, line, blank)
85
89
  end
@@ -8,6 +8,7 @@ module ToonFu
8
8
  @strict = strict
9
9
  @indent_size = indent_size
10
10
  @depth = 0
11
+ @in_span = false
11
12
  end
12
13
 
13
14
  def read(text)
@@ -21,18 +22,19 @@ module ToonFu
21
22
 
22
23
  def root
23
24
  line = @lines.peek
25
+ return object(-1, 0) unless line.depth.zero?
24
26
  return empty_array_root if line.content == "[]"
25
27
 
26
28
  header = usable(Header.parse(line.content), line)
27
29
  return keyless_root(header) if header && header.key.nil?
28
- return scalar_root(line) if @lines.one? && Tokens.colon_index(line.content).nil?
30
+ return scalar_root(line) if @lines.one? && scalar?(line)
29
31
 
30
- object(0)
32
+ object(-1, 0)
31
33
  end
32
34
 
33
35
  def keyless_root(header)
34
36
  @lines.next
35
- value = header.fields? ? table(header, 1) : array(header, 1)
37
+ value = header.fields? ? table(header, 0) : array(header, 0)
36
38
  trailing
37
39
  value
38
40
  end
@@ -54,26 +56,30 @@ module ToonFu
54
56
  raise Error, "cannot decode content after the root form: #{@lines.peek.text}"
55
57
  end
56
58
 
57
- def object(depth, result = {})
58
- while (line = @lines.peek) && line.depth >= depth
59
- if line.depth > depth
59
+ def object(parent, depth, result = {})
60
+ while (line = @lines.peek) && line.depth > parent
61
+ if line.depth == depth
62
+ take
63
+ key, value = field(line, depth)
64
+ store(result, key, value, line)
65
+ else
60
66
  orphan(line)
61
- next
62
67
  end
63
-
64
- @lines.next
65
- key, value = field(line, depth)
66
- store(result, key, value, line)
67
68
  end
68
69
  result
69
70
  end
70
71
 
71
72
  def orphan(line)
72
73
  raise Error, "cannot decode a line that belongs to no scope on line #{line.number}: #{line.text}" if @strict
74
+ raise Error, "cannot decode a bare token line inside a scope on line #{line.number}: #{line.text}" if scalar?(line)
73
75
 
74
76
  @lines.next
75
77
  end
76
78
 
79
+ def scalar?(line)
80
+ Tokens.colon_index(line.content).nil?
81
+ end
82
+
77
83
  def store(result, key, value, line)
78
84
  raise Error, "cannot decode a duplicate key on line #{line.number}: #{key.inspect}" if @strict && result.key?(key)
79
85
 
@@ -83,7 +89,7 @@ module ToonFu
83
89
  def field(line, depth)
84
90
  header = usable(Header.parse(line.content), line)
85
91
  return [Tokens.key(header.key), header_value(header, depth)] if header&.key
86
- raise Error, "cannot decode a keyless array header in object field position on line #{line.number}: #{line.text}" if header
92
+ misplaced(line, "a keyless array header in object field position") if header
87
93
 
88
94
  colon = Tokens.colon_index(line.content)
89
95
  raise Error, "cannot decode a line without a colon after its key on line #{line.number}: #{line.text}" if colon.nil?
@@ -99,30 +105,23 @@ module ToonFu
99
105
  Token.decode(text)
100
106
  end
101
107
 
102
- def nested(depth)
103
- line = @lines.peek
104
- return {} if line.nil? || line.depth <= depth
108
+ def nested(parent)
109
+ depth = content_depth(parent)
110
+ return {} if depth.nil?
105
111
 
106
- guard(line)
107
- descend { object(depth + 1) }
112
+ descend { object(parent, depth) }
108
113
  end
109
114
 
110
115
  def header_value(header, depth)
111
- return table(header, depth + 1) if header.fields?
112
- return [] if header.length.zero? && !items?(depth + 1)
113
-
114
- array(header, depth + 1)
116
+ header.fields? ? table(header, depth) : array(header, depth)
115
117
  end
116
118
 
117
- def items?(depth)
118
- line = @lines.peek
119
- !line.nil? && line.depth == depth
120
- end
121
-
122
- def array(header, depth)
119
+ def array(header, parent)
123
120
  return inline(header) unless header.inline.empty?
124
121
 
125
- descend { list(header, depth) }
122
+ items = descend { gather(parent, stop: ->(line) { !line.item? }) { |line| item(line) } }
123
+ count(items.length, header, "item")
124
+ items
126
125
  end
127
126
 
128
127
  def inline(header)
@@ -131,46 +130,27 @@ module ToonFu
131
130
  cells
132
131
  end
133
132
 
134
- def list(header, depth)
135
- items = []
136
- while (line = @lines.peek) && line.depth == depth && line.item?
137
- span(line) unless items.empty?
138
- @lines.next
139
- items << item(line, depth)
140
- end
141
- scalar_line(@lines.peek, depth)
142
- count(items.length, header, "item")
143
- items
144
- end
145
-
146
- # A scalar line is valid only as a root primitive; inside a scope it is a
147
- # structural error in strict and non-strict mode alike (§5.2, §14.2).
148
- def scalar_line(line, depth)
149
- return if line.nil? || line.depth != depth || Tokens.colon_index(line.content)
150
-
151
- header = Header.parse(line.content)
152
- return if header && !header.malformed?
153
-
154
- raise Error, "cannot decode a bare token line inside a scope on line #{line.number}: #{line.text}"
155
- end
156
-
157
- def item(line, depth)
133
+ def item(line)
158
134
  rest = line.item_content
159
135
  return {} if rest.empty?
160
136
  return [] if rest == "[]"
161
137
 
162
138
  header = usable(Header.parse(rest), line)
163
- return item_header(header, line, depth) if header
164
- return item_object(rest, depth) if Tokens.colon_index(rest)
139
+ return item_header(header, line) if header
140
+ return item_object(rest, line.depth) if Tokens.colon_index(rest)
165
141
 
166
142
  Token.decode(Tokens.trim(rest))
167
143
  end
168
144
 
169
- def item_header(header, line, depth)
170
- return item_object(line.item_content, depth) if header.key
171
- raise Error, "cannot decode a keyless fields-bearing header as a list item on line #{line.number}: #{line.text}" if header.fields?
145
+ def item_header(header, line)
146
+ return array(header, line.depth) unless header.key || header.fields?
147
+
148
+ misplaced(line, "a keyless fields-bearing header as a list item") unless header.key
149
+ item_object(line.item_content, line.depth)
150
+ end
172
151
 
173
- array(header, depth + 1)
152
+ def misplaced(line, what)
153
+ raise Error, "cannot decode #{what} on line #{line.number}: #{line.text}" if @strict
174
154
  end
175
155
 
176
156
  def item_object(rest, depth)
@@ -178,20 +158,18 @@ module ToonFu
178
158
  key, value = descend { field(synthetic, depth + 1) }
179
159
  result = {}
180
160
  store(result, key, value, synthetic)
181
- following = @lines.peek
182
- span(following) if following && following.depth == depth + 1
183
- descend { object(depth + 1, result) }
161
+ descend { object(depth, depth + 1, result) }
184
162
  end
185
163
 
186
- def table(header, depth)
164
+ def table(header, parent)
187
165
  raise Error, "cannot decode a field name repeated in one field list: #{header.duplicate.inspect}" if @strict && header.duplicate
188
166
 
189
- header.keyed? ? entries(header, depth) : rows(header, depth)
167
+ header.keyed? ? entries(header, parent) : rows(header, parent)
190
168
  end
191
169
 
192
- def rows(header, depth)
170
+ def rows(header, parent)
193
171
  collected = descend do
194
- gather(depth, stop: ->(line) { key_value_line?(line, header.delimiter) }) { |line| row(header, line) }
172
+ gather(parent, stop: ->(line) { key_value_line?(line, header.delimiter) }) { |line| row(header, line) }
195
173
  end
196
174
  count(collected.length, header, "row")
197
175
  collected
@@ -207,10 +185,10 @@ module ToonFu
207
185
  cell.nil? || colon < cell
208
186
  end
209
187
 
210
- def entries(header, depth)
188
+ def entries(header, parent)
211
189
  result = {}
212
190
  collected = descend do
213
- gather(depth) do |line|
191
+ gather(parent) do |line|
214
192
  colon = Tokens.colon_index(line.content)
215
193
  if colon.nil?
216
194
  raise Error, "cannot decode an entry row without a colon on line #{line.number}: #{line.text}" if @strict
@@ -226,22 +204,29 @@ module ToonFu
226
204
  result
227
205
  end
228
206
 
229
- def gather(depth, stop: nil)
207
+ def gather(parent, stop: nil)
208
+ outer = @in_span
230
209
  collected = []
231
- while (line = @lines.peek) && line.depth == depth
210
+ depth = content_depth(parent)
211
+ while depth && (line = @lines.peek) && line.depth > parent
212
+ if line.depth != depth
213
+ orphan(line)
214
+ next
215
+ end
232
216
  break if stop&.call(line)
233
217
 
234
- span(line) unless collected.empty?
235
- @lines.next
218
+ take
219
+ @in_span = true
236
220
  collected << yield(line)
237
221
  end
238
222
  collected
223
+ ensure
224
+ @in_span = outer
239
225
  end
240
226
 
241
- def span(line)
242
- return unless @strict && line.after_blank
243
-
244
- raise Error, "cannot decode a blank line inside a header span, before line #{line.number}: #{line.text}"
227
+ def take
228
+ line = @lines.next
229
+ raise Error, "cannot decode a blank line inside a header span, before line #{line.number}: #{line.text}" if @strict && @in_span && line.after_blank
245
230
  end
246
231
 
247
232
  def row(header, line, text = line.content)
@@ -285,10 +270,12 @@ module ToonFu
285
270
  nil
286
271
  end
287
272
 
288
- def guard(line)
289
- return unless @strict && line.depth > @depth + 1
273
+ def content_depth(parent)
274
+ line = @lines.peek
275
+ return if line.nil? || line.depth <= parent
276
+ raise Error, "cannot decode a depth jump on line #{line.number}: #{line.text}" if @strict && line.depth > parent + 1
290
277
 
291
- raise Error, "cannot decode a depth jump on line #{line.number}: #{line.text}"
278
+ line.depth
292
279
  end
293
280
 
294
281
  def descend
@@ -19,6 +19,10 @@ module ToonFu
19
19
  quote?(string) ? quote(string) : string
20
20
  end
21
21
 
22
+ def root(string)
23
+ string.start_with?(Lines::BOM) ? quote(string) : encode(string)
24
+ end
25
+
22
26
  def key(string)
23
27
  BARE_KEY.match?(string) ? string : quote(string)
24
28
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module ToonFu
4
4
  module Tokens
5
- QUOTED = /"(?:[^"\\]|\\.)*"/
5
+ QUOTE_SPAN = /"(?:[^"\\]|\\.?)*"?/
6
+ TRAILING_WHITESPACE = /[[:space:]]\z/
6
7
  UNTIL_COLON = /[^":]+/
7
8
  UNTIL_BRACKET = /[^":\[]+/
8
9
  UNTIL_BRACE = /[^"{]+/
@@ -17,7 +18,7 @@ module ToonFu
17
18
  until scanner.eos?
18
19
  next if scanner.skip(pattern)
19
20
  return scanner.charpos if stops.include?(scanner.peek(1))
20
- return nil unless scanner.skip(QUOTED)
21
+ scanner.skip(QUOTE_SPAN)
21
22
  end
22
23
  nil
23
24
  end
@@ -40,7 +41,7 @@ module ToonFu
40
41
  depth = 0
41
42
  scanner = StringScanner.new(text)
42
43
  until scanner.eos?
43
- if scanner.skip(QUOTED)
44
+ if scanner.skip(QUOTE_SPAN)
44
45
  next
45
46
  elsif scanner.skip(/\{/)
46
47
  depth += 1
@@ -48,7 +49,7 @@ module ToonFu
48
49
  depth -= 1
49
50
  return scanner.charpos - 1 if depth.zero?
50
51
  else
51
- scanner.skip(/[^"{}]+/) || scanner.skip(/./)
52
+ scanner.skip(/[^"{}]+/)
52
53
  end
53
54
  end
54
55
  nil
@@ -60,7 +61,7 @@ module ToonFu
60
61
  depth = 0
61
62
  scanner = StringScanner.new(text)
62
63
  until scanner.eos?
63
- if (quoted = scanner.scan(QUOTED))
64
+ if (quoted = scanner.scan(QUOTE_SPAN))
64
65
  current << quoted
65
66
  elsif depth.zero? && scanner.skip(delimiter)
66
67
  entries << current
@@ -81,13 +82,13 @@ module ToonFu
81
82
  current = +""
82
83
  scanner = StringScanner.new(text)
83
84
  until scanner.eos?
84
- if (quoted = scanner.scan(QUOTED))
85
+ if (quoted = scanner.scan(QUOTE_SPAN))
85
86
  current << quoted
86
87
  elsif scanner.skip(delimiter)
87
88
  cells << current
88
89
  current = +""
89
90
  else
90
- current << (scanner.scan(UNTIL_CELL_END.fetch(delimiter)) || scanner.getch)
91
+ current << scanner.scan(UNTIL_CELL_END.fetch(delimiter))
91
92
  end
92
93
  end
93
94
  cells << current
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ToonFu
4
- VERSION = "4.1.4"
4
+ VERSION = "4.3.0"
5
5
  end
@@ -23,6 +23,7 @@ module ToonFu
23
23
  case value
24
24
  when Hash then mapping("", value)
25
25
  when Array then array("", value)
26
+ when String then line(@strings.root(value))
26
27
  else line(scalar(value))
27
28
  end
28
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toon-fu
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.4
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yevhenii Hurin