perfect_toml24 0.9.1

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: '08c079df257b180663dc390f14cac5af24fe74019502e4bdc482b2980d9813d9'
4
+ data.tar.gz: b4849dcec662dcf8e663754f1f7c255a60b373df3dc3d86fe7ec5fc515b686b9
5
+ SHA512:
6
+ metadata.gz: 861a3f88352278d4d3834699604b569c26c1b760a8975a4e9241b41b38e52e7fdb6c520ea63f3f71520f14932b88c7c385fad6a8ddc92627f8d94d25da5c5c9b
7
+ data.tar.gz: ddb0ab92c39113bc20dc55da168157254638832f408f8eebebdb4fcae112c28b8cce9a087195be8682af12d2b4fbd57f7e0d99ad9318e9bd46fd60b8a5529ea6
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # PerfectTOML Changelog
2
+
3
+ ## 0.9.1 / 2025-04-08
4
+
5
+ * Support Ruby 2.4
6
+
7
+ ## 0.9.0 / 2022-07-17
8
+
9
+ * First release.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake", "~> 13.0"
6
+
7
+ gem "test-unit", "~> 3.0"
8
+
9
+ gem "simplecov", "~> 0.21.2"
10
+
11
+ gem "json", ">= 2.7.2"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Yusuke Endoh
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/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Yusuke Endoh
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # PerfectTOML
2
+
3
+ Yet another [TOML](https://github.com/toml-lang/toml) parser and generator.
4
+
5
+ Features:
6
+
7
+ * Fully compliant with [TOML v1.0.0](https://toml.io/en/v1.0.0). It passes [toml-lang/toml-test](https://github.com/toml-lang/toml-test).
8
+ * Faster than existing TOML parsers for Ruby. See [Benchmark](#benchmark).
9
+ * Single-file, plain old Ruby script without any dependencies: [perfect_toml.rb](https://github.com/mame/perfect_toml/blob/master/lib/perfect_toml.rb).
10
+
11
+ ## Installation
12
+
13
+ Install the gem and add to the application's Gemfile by executing:
14
+
15
+ $ bundle add perfect_toml
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ $ gem install perfect_toml
20
+
21
+ ## Parser Usage
22
+
23
+ ```ruby
24
+ require "perfect_toml"
25
+
26
+ # Decodes a TOML string
27
+ p PerfectTOML.parse("key = 42") #=> { "key" => 42 }
28
+
29
+ # Load a TOML file
30
+ PerfectTOML.load_file("file.toml")
31
+
32
+ # If you want Symbol keys:
33
+ PerfectTOML.load_file("file.toml", symbolize_names: true)
34
+ ```
35
+
36
+ ## Generator Usage
37
+
38
+ ```ruby
39
+ require "perfect_toml"
40
+
41
+ # Encode a Hash in TOML format
42
+ p PerfectTOML.generate({ key: 42 }) #=> "key = 42\n"
43
+
44
+ # Save a Hash in a TOML file
45
+ PerfectTOML.save_file("file.toml", { key: 42 })
46
+ ```
47
+
48
+ See the document for options.
49
+
50
+ ## TOML's value vs. Ruby's value
51
+
52
+ TOML's table is converted to Ruby's Hash, and vice versa.
53
+ Other most TOML values are converted to an object of Ruby class of the same name:
54
+ for example, TOML's String corresponds to Ruby's String.
55
+ Because there are no classes corresponding to TOML's Local Date-Time, Local Date, and Local Time,
56
+ PerfectTOML provides dedicated classes, respectively,
57
+ `PerfectTOML::LocalDateTime`, `PerfectTOML::LocalDate`, and `PerfectTOML::LocalTime`.
58
+
59
+ ```ruby
60
+ require "perfect_toml"
61
+
62
+ p PerfectTOML.parse("local-date = 1970-01-01")
63
+ #=> { "local-date" => #<PerfectTOML::LocalDate 1970-01-01> }
64
+ ```
65
+
66
+ ## Benchmark
67
+
68
+ PerfectTOML is 5x faster than [tomlrb](https://github.com/fbernier/tomlrb), and 100x faster than [toml-rb](https://github.com/emancu/toml-rb).
69
+
70
+ ```ruby
71
+ require "benchmark/ips"
72
+ require_relative "lib/perfect_toml"
73
+ require "toml-rb"
74
+ require "tomlrb"
75
+
76
+ # https://raw.githubusercontent.com/toml-lang/toml/v0.5.0/examples/example-v0.4.0.toml
77
+ data = File.read("example-v0.4.0.toml")
78
+
79
+ Benchmark.ips do |x|
80
+ x.report("emancu/toml-rb") { TomlRB.parse(data) }
81
+ x.report("fbernier/tomlrb") { Tomlrb.parse(data) }
82
+ x.report("mame/perfect_toml") { PerfectTOML.parse(data) }
83
+ x.compare!
84
+ end
85
+ ```
86
+
87
+ ```
88
+ ...
89
+ Comparison:
90
+ mame/perfect_toml: 2982.5 i/s
91
+ fbernier/tomlrb: 515.7 i/s - 5.78x (± 0.00) slower
92
+ emancu/toml-rb: 25.4 i/s - 117.36x (± 0.00) slower
93
+ ```
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mame/perfect_toml.
98
+
99
+ ## License
100
+
101
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "rdoc/task"
4
+
5
+ Rake::TestTask.new(:core_test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ TOML_TEST = "./toml-test-v1.2.0-linux-amd64"
12
+
13
+ file TOML_TEST do
14
+ require "open-uri"
15
+ require "zlib"
16
+ URI.open("https://github.com/BurntSushi/toml-test/releases/download/v1.2.0/toml-test-v1.2.0-linux-amd64.gz", "rb") do |f|
17
+ File.binwrite(TOML_TEST, Zlib::GzipReader.new(f).read)
18
+ File.chmod(0o755, TOML_TEST)
19
+ end
20
+ end
21
+
22
+ task :toml_decoder_test => TOML_TEST do
23
+ sh "./toml-test-v1.2.0-linux-amd64", "./tool/decoder.rb"
24
+ end
25
+
26
+ task :toml_encoder_test => TOML_TEST do
27
+ ["0000", "1000", "0010", "0001", "0011"].each do |mode|
28
+ ENV["TOML_ENCODER_USE_DOT"] = mode[0]
29
+ ENV["TOML_ENCODER_SORT_KEYS"] = mode[1]
30
+ ENV["TOML_ENCODER_USE_LITERAL_STRING"] = mode[2]
31
+ ENV["TOML_ENCODER_USE_MULTILINE_STRING"] = mode[3]
32
+ sh "./toml-test-v1.2.0-linux-amd64", "./tool/encoder.rb", "--encoder", "-skip", "valid/string/multiline-quotes"
33
+ end
34
+ end
35
+
36
+ task :test => [:core_test, :toml_decoder_test, :toml_encoder_test]
37
+
38
+ task default: :test
39
+
40
+ Rake::RDocTask.new do |rdoc|
41
+ files =["README.md", "LICENSE", "lib/perfect_toml.rb"]
42
+ rdoc.rdoc_files.add(files)
43
+ rdoc.main = "README.md"
44
+ rdoc.title = "PerfectTOML Docs"
45
+ rdoc.rdoc_dir = "doc/rdoc"
46
+ end
@@ -0,0 +1,961 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2022 Yusuke Endoh
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.
22
+
23
+ require "strscan"
24
+
25
+ module PerfectTOML
26
+ VERSION = "0.9.1"
27
+
28
+ class LocalDateTimeBase
29
+ def to_inline_toml
30
+ to_s
31
+ end
32
+
33
+ def inspect
34
+ "#<#{ self.class } #{ to_s }>"
35
+ end
36
+
37
+ def pretty_print(q)
38
+ q.text inspect
39
+ end
40
+
41
+ def ==(other)
42
+ self.class == other.class &&
43
+ [:year, :month, :day, :hour, :min, :sec].all? do |key|
44
+ !respond_to?(key) || (send(key) == other.send(key))
45
+ end
46
+ end
47
+
48
+ include Comparable
49
+
50
+ def <=>(other)
51
+ return nil if self.class != other.class
52
+ [:year, :month, :day, :hour, :min, :sec].each do |key|
53
+ next unless respond_to?(key)
54
+ cmp = send(key) <=> other.send(key)
55
+ return cmp if cmp != 0
56
+ end
57
+ return 0
58
+ end
59
+ end
60
+
61
+ # Represents TOML's Local Date-Time
62
+ #
63
+ # See https://toml.io/en/v1.0.0#local-date-time
64
+ class LocalDateTime < LocalDateTimeBase
65
+ def initialize(year, month, day, hour, min, sec)
66
+ @year = year.to_i
67
+ @month = month.to_i
68
+ @day = day.to_i
69
+ @hour = hour.to_i
70
+ @min = min.to_i
71
+ @sec = Numeric === sec ? sec : sec.include?(".") ? Rational(sec) : sec.to_i
72
+ end
73
+
74
+ attr_reader :year, :month, :day, :hour, :min, :sec
75
+
76
+ # Converts to a Time object with the local timezone.
77
+ #
78
+ # ldt = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4)
79
+ # ldt.to_time #=> 1970-01-01 02:03:04 +0900
80
+ #
81
+ # You can specify timezone by passing an argument.
82
+ #
83
+ # ldt.to_time("UTC") #=> 1970-01-01 02:03:04 UTC
84
+ # ldt.to_time("+00:00") #=> 1970-01-01 02:03:04 +0000
85
+ def to_time(zone = nil)
86
+ @time = Time.new(@year, @month, @day, @hour, @min, @sec, zone)
87
+ end
88
+
89
+ # Returns a string representation in RFC 3339 format
90
+ #
91
+ # ldt = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4)
92
+ # ldt.to_s #=> 1970-01-01T02:03:04
93
+ def to_s
94
+ s = "%04d-%02d-%02dT%02d:%02d:%02d" % [@year, @month, @day, @hour, @min, @sec]
95
+ s << ("%11.9f" % (@sec - @sec.floor))[1..-1] unless Integer === @sec
96
+ s
97
+ end
98
+ end
99
+
100
+ # Represents TOML's Local Date
101
+ #
102
+ # See https://toml.io/en/v1.0.0#local-date
103
+ class LocalDate < LocalDateTimeBase
104
+ def initialize(year, month, day)
105
+ @year = year.to_i
106
+ @month = month.to_i
107
+ @day = day.to_i
108
+ end
109
+
110
+ attr_reader :year, :month, :day
111
+
112
+ # Converts to a Time object with the local timezone.
113
+ # Its time will be 00:00:00.
114
+ #
115
+ # ld = PerfectTOML::LocalDate.new(1970, 1, 1)
116
+ # ld.to_time #=> 1970-01-01 00:00:00 +0900
117
+ #
118
+ # You can specify timezone by passing an argument.
119
+ #
120
+ # ld.to_time("UTC") #=> 1970-01-01 00:00:00 UTC
121
+ # ld.to_time("+00:00") #=> 1970-01-01 00:00:00 +0000
122
+ def to_time(zone = nil)
123
+ Time.new(@year, @month, @day, 0, 0, 0, zone)
124
+ end
125
+
126
+ # Returns a string representation in RFC 3339 format
127
+ #
128
+ # ld = PerfectTOML::LocalDate.new(1970, 1, 1)
129
+ # ld.to_s #=> 1970-01-01
130
+ def to_s
131
+ "%04d-%02d-%02d" % [@year, @month, @day]
132
+ end
133
+ end
134
+
135
+
136
+ # Represents TOML's Local Time
137
+ #
138
+ # See https://toml.io/en/v1.0.0#local-time
139
+ class LocalTime < LocalDateTimeBase
140
+ def initialize(hour, min, sec)
141
+ @hour = hour.to_i
142
+ @min = min.to_i
143
+ @sec = Numeric === sec ? sec : sec.include?(".") ? Rational(sec) : sec.to_i
144
+ end
145
+
146
+ attr_reader :hour, :min, :sec
147
+
148
+ # Converts to a Time object with the local timezone.
149
+ # You need to specify year, month, and day.
150
+ #
151
+ # ld = PerfectTOML::LocalTime.new(2, 3, 4)
152
+ # ld.to_time(1970, 1, 1) #=> 1970-01-01 02:03:04 +0900
153
+ #
154
+ # You can specify timezone by passing the fourth argument.
155
+ #
156
+ # ld.to_time(1970, 1, 1, "UTC") #=> 1970-01-01 02:03:04 UTC
157
+ # ld.to_time(1970, 1, 1, "+00:00") #=> 1970-01-01 02:03:04 +0000
158
+ def to_time(year, month, day, zone = nil)
159
+ Time.new(year, month, day, @hour, @min, @sec, zone)
160
+ end
161
+
162
+ # Returns a string representation in RFC 3339 format
163
+ #
164
+ # lt = PerfectTOML::LocalTime.new(2, 3, 4)
165
+ # lt.to_s #=> 02:03:04
166
+ def to_s
167
+ s = "%02d:%02d:%02d" % [@hour, @min, @sec]
168
+ s << ("%11.9f" % (@sec - @sec.floor))[1..-1] unless Integer === @sec
169
+ s
170
+ end
171
+ end
172
+
173
+ # call-seq:
174
+ #
175
+ # parse(toml_src, symbolize_names: boolean) -> Object
176
+ #
177
+ # Decodes a TOML string.
178
+ #
179
+ # PerfectTOML.parse("key = 42") #=> { "key" => 42 }
180
+ #
181
+ # src = <<~END
182
+ # [foo]
183
+ # bar = "baz"
184
+ # END
185
+ # PerfectTOML.parse(src) #=> { "foo" => { "bar" => "baz" } }
186
+ #
187
+ # All keys in the Hash are String by default.
188
+ # If a keyword `symbolize_names` is specficied as truthy,
189
+ # all keys in the Hash will be Symbols.
190
+ #
191
+ # PerfectTOML.parse("key = 42", symbolize_names: true) #=> { :key => 42 }
192
+ #
193
+ # src = <<~END
194
+ # [foo]
195
+ # bar = "baz"
196
+ # END
197
+ # PerfectTOML.parse(src, symbolize_names: true) #=> { :key => { :bar => "baz" } }
198
+ def self.parse(toml_src, **opts)
199
+ Parser.new(toml_src, **opts).parse
200
+ end
201
+
202
+ # call-seq:
203
+ #
204
+ # load_file(filename, symbolize_names: boolean) -> Object
205
+ #
206
+ # Loads a TOML file.
207
+ #
208
+ # # test.toml
209
+ # # key = 42
210
+ # PerfectTOML.load_file("test.toml") #=> { "key" => 42 }
211
+ #
212
+ # See PerfectTOML.parse for options.
213
+ def self.load_file(io, **opts)
214
+ io = File.open(io, encoding: "UTF-8") unless IO === io
215
+
216
+ parse(io.read, **opts)
217
+ end
218
+
219
+ # call-seq:
220
+ #
221
+ # generate(hash, sort_keys: false, use_literal_string: false, use_multiline_string: false, use_dot: false) -> String
222
+ #
223
+ # Encode a Hash in TOML format.
224
+ #
225
+ # PerfectTOML.generate({ key: 42 })
226
+ # # output:
227
+ # # key = 42
228
+ #
229
+ # The order of hashes are respected by default.
230
+ # If you want to sort them, you can use +sort_keys+ keyword:
231
+ #
232
+ # PerfectTOML.generate({ z: 1, a: 2 })
233
+ # # output:
234
+ # # z = 1
235
+ # # a = 2
236
+ #
237
+ # PerfectTOML.generate({ z: 1, a: 2 }, sort_keys: true)
238
+ # # output:
239
+ # # a = 2
240
+ # # z = 1
241
+ #
242
+ # By default, all strings are quoted by quotation marks.
243
+ # If +use_literal_string+ keyword is specified as truthy,
244
+ # it prefers a literal string quoted by single quotes:
245
+ #
246
+ # PerfectTOML.generate({ a: "foo" })
247
+ # # output:
248
+ # # a = "foo"
249
+ #
250
+ # PerfectTOML.generate({ a: "foo" }, use_literal_string: true)
251
+ # # output:
252
+ # # a = 'foo'
253
+ #
254
+ # Multiline strings are not used by default.
255
+ # If +use_multiline_string+ keyword is specified as truthy,
256
+ # it uses a multiline string if the string contains a newline.
257
+ #
258
+ # PerfectTOML.generate({ a: "foo\nbar" })
259
+ # # output:
260
+ # # a = "foo\nbar"
261
+ #
262
+ # PerfectTOML.generate({ a: "foo\nbar" }, use_multiline_string: true)
263
+ # # output:
264
+ # # a = """
265
+ # # foo
266
+ # # bar"
267
+ #
268
+ # By default, dotted keys are used only in a header.
269
+ # If +use_dot+ keyword is specified as truthy,
270
+ # it uses a dotted key only when a subtree does not branch.
271
+ #
272
+ # PerfectTOML.generate({ a: { b: { c: { d: 42 } } } })
273
+ # # output:
274
+ # # [a.b.c]
275
+ # # d = 42
276
+ #
277
+ # PerfectTOML.generate({ a: { b: { c: { d: 42 } } } }, use_dot: true)
278
+ # # output:
279
+ # # a.b.c.d = 42
280
+ def self.generate(hash, **opts)
281
+ out = ""
282
+ Generator.new(hash, out, **opts).generate
283
+ out
284
+ end
285
+
286
+ # call-seq:
287
+ #
288
+ # save_file(filename, hash, symbolize_names: boolean) -> Object
289
+ #
290
+ # Saves a Hash into a file in TOML format.
291
+ #
292
+ # PerfectTOML.save_file("out.toml", { key: 42 })
293
+ # # out.toml
294
+ # # key = 42
295
+ #
296
+ # See PerfectTOML.generate for options.
297
+ def self.save_file(io, hash, **opts)
298
+ io = File.open(io, mode: "w", encoding: "UTF-8") unless IO === io
299
+ Generator.new(hash, io, **opts).generate
300
+ ensure
301
+ io.close
302
+ end
303
+
304
+ class ParseError < StandardError; end
305
+
306
+ class Parser # :nodoc:
307
+ def initialize(src, symbolize_names: false)
308
+ @s = StringScanner.new(src)
309
+ @symbolize_names = symbolize_names
310
+ @root_node = @topic_node = Node.new(1, nil)
311
+ end
312
+
313
+ def parse
314
+ parse_toml
315
+ end
316
+
317
+ private
318
+
319
+ # error handling
320
+
321
+ def error(msg)
322
+ prev = @s.string.byteslice(0, @s.pos)
323
+ last_newline = prev.rindex("\n")
324
+ bol = last_newline ? prev[0, last_newline + 1].bytesize : 0
325
+ lineno = prev.count("\n") + 1
326
+ column = @s.pos - bol + 1
327
+ raise ParseError, "#{ msg } at line %d column %d" % [lineno, column]
328
+ end
329
+
330
+ def unterminated_string_error
331
+ error "unterminated string"
332
+ end
333
+
334
+ def unexpected_error
335
+ if @s.eos?
336
+ error "unexpected end"
337
+ elsif @s.scan(/[A-Za-z0-9_\-]+/)
338
+ str = @s[0]
339
+ @s.unscan
340
+ error "unexpected identifier found: #{ str.dump }"
341
+ else
342
+ error "unexpected character found: #{ @s.peek(1).dump }"
343
+ end
344
+ end
345
+
346
+ def redefine_key_error(keys, dup_key)
347
+ @s.pos = @keys_start_pos
348
+ keys = (keys + [dup_key]).map {|key| Generator.escape_key(key) }.join(".")
349
+ error "cannot redefine `#{ keys }`"
350
+ end
351
+
352
+ # helpers for parsing
353
+
354
+ def skip_spaces
355
+ @s.skip(/(?:[\t\n ]|\r\n|#[^\x00-\x08\x0a-\x1f\x7f]*(?:\n|\r\n))+/)
356
+
357
+ skip_comment if @s.check(/#/)
358
+ end
359
+
360
+ def skip_comment
361
+ return if @s.skip(/#[^\x00-\x08\x0a-\x1f\x7f]*(?:\n|\r\n|\z)/)
362
+
363
+ @s.skip(/[^\x00-\x08\x0a-\x1f\x7f]*/)
364
+ unexpected_error
365
+ end
366
+
367
+ def skip_rest_of_line
368
+ @s.skip(/[\t ]+/)
369
+ case
370
+ when @s.check(/#/) then skip_comment
371
+ when @s.skip(/\n|\r\n/) || @s.eos?
372
+ else
373
+ unexpected_error
374
+ end
375
+ end
376
+
377
+ # parsing for strings
378
+
379
+ ESCAPE_CHARS = {
380
+ ?b => ?\b, ?t => ?\t, ?n => ?\n, ?f => ?\f, ?r => ?\r, ?" => ?", ?\\ => ?\\
381
+ }
382
+
383
+ def parse_escape_char
384
+ if @s.skip(/\\/)
385
+ if @s.skip(/([btnmfr"\\])|u([0-9A-Fa-f]{4})|U([0-9A-Fa-f]{8})/)
386
+ @s[1] ? ESCAPE_CHARS[@s[1]] : (@s[2] || @s[3]).hex.chr("UTF-8")
387
+ else
388
+ unterminated_string_error if @s.eos?
389
+ error "invalid escape character in string: #{ @s.peek(1).dump }"
390
+ end
391
+ else
392
+ unterminated_string_error if @s.eos?
393
+ error "invalid character in string: #{ @s.peek(1).dump }"
394
+ end
395
+ end
396
+
397
+ def parse_basic_string
398
+ str = ""
399
+ while true
400
+ str << @s.scan(/[^\x00-\x08\x0a-\x1f\x7f"\\]*/)
401
+ return str if @s.skip(/"/)
402
+ str << parse_escape_char
403
+ end
404
+ end
405
+
406
+ def parse_multiline_basic_string
407
+ # skip a newline
408
+ @s.skip(/\n|\r\n/)
409
+
410
+ str = ""
411
+ while true
412
+ str << @s.scan(/[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f"\\]*/)
413
+ delimiter = @s.skip(/"{1,5}/)
414
+ if delimiter
415
+ str << "\"" * (delimiter % 3)
416
+ return str if delimiter >= 3
417
+ next
418
+ end
419
+ next if @s.skip(/\\[\t ]*(?:\n|\r\n)(?:[\t\n ]|\r\n)*/)
420
+ str << parse_escape_char
421
+ end
422
+ end
423
+
424
+ def parse_literal_string
425
+ str = @s.scan(/[^\x00-\x08\x0a-\x1f\x7f']*/)
426
+ return str if @s.skip(/'/)
427
+ unterminated_string_error if @s.eos?
428
+ error "invalid character in string: #{ @s.peek(1).dump }"
429
+ end
430
+
431
+ def parse_multiline_literal_string
432
+ # skip a newline
433
+ @s.skip(/\n|\r\n/)
434
+
435
+ str = ""
436
+ while true
437
+ str << @s.scan(/[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f']*/)
438
+ if delimiter = @s.skip(/'{1,5}/)
439
+ str << "'" * (delimiter % 3)
440
+ return str if delimiter >= 3
441
+ next
442
+ end
443
+ unterminated_string_error if @s.eos?
444
+ error "invalid character in string: #{ @s.peek(1).dump }"
445
+ end
446
+ end
447
+
448
+ # parsing for date/time
449
+
450
+ def parse_datetime(preread_len)
451
+ str = @s[0]
452
+ pos = @s.pos - preread_len
453
+ year, month, day = @s[1], @s[2], @s[3]
454
+ if @s.skip(/[T ](\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/i)
455
+ str << @s[0]
456
+ hour, min, sec = @s[1], @s[2], @s[3]
457
+ raise ArgumentError unless (0..23).cover?(hour.to_i)
458
+ zone = @s.scan(/(Z)|[-+]\d{2}:\d{2}/i)
459
+ time = Time.new(year, month, day, hour, min, sec.to_r, @s[1] ? "UTC" : zone)
460
+ if zone
461
+ time
462
+ else
463
+ LocalDateTime.new(year, month, day, hour, min, sec)
464
+ end
465
+ else
466
+ Time.new(year, month, day, 0, 0, 0, "Z") # parse check
467
+ LocalDate.new(year, month, day)
468
+ end
469
+ rescue ArgumentError
470
+ @s.pos = pos
471
+ error "failed to parse date or datetime \"#{ str }\""
472
+ end
473
+
474
+ def parse_time(preread_len)
475
+ hour, min, sec = @s[1], @s[2], @s[3]
476
+ Time.new(1970, 1, 1, hour, min, sec.to_r, "Z") # parse check
477
+ LocalTime.new(hour, min, sec)
478
+ rescue ArgumentError
479
+ @s.pos -= preread_len
480
+ error "failed to parse time \"#{ @s[0] }\""
481
+ end
482
+
483
+ # parsing for inline array/table
484
+
485
+ def parse_array
486
+ ary = []
487
+ while true
488
+ skip_spaces
489
+ break if @s.skip(/\]/)
490
+ ary << parse_value
491
+ skip_spaces
492
+ next if @s.skip(/,/)
493
+ break if @s.skip(/\]/)
494
+ unexpected_error
495
+ end
496
+ ary
497
+ end
498
+
499
+ def parse_inline_table
500
+ @s.skip(/[\t ]*/)
501
+ if @s.skip(/\}/)
502
+ {}
503
+ else
504
+ tmp_node = Node.new(1, nil)
505
+ while true
506
+ @keys_start_pos = @s.pos
507
+ keys = parse_keys
508
+ @s.skip(/[\t ]*/)
509
+ unexpected_error unless @s.skip(/=[\t ]*/)
510
+ define_value(tmp_node, keys)
511
+ @s.skip(/[\t ]*/)
512
+ next if @s.skip(/,[\t ]*/)
513
+ break if @s.skip(/\}/)
514
+ unexpected_error
515
+ end
516
+ tmp_node.table
517
+ end
518
+ end
519
+
520
+ # parsing key and value
521
+
522
+ def parse_keys
523
+ keys = []
524
+ while true
525
+ case
526
+ when key = @s.scan(/[A-Za-z0-9_\-]+/)
527
+ when @s.skip(/"/) then key = parse_basic_string
528
+ when @s.skip(/'/) then key = parse_literal_string
529
+ else
530
+ unexpected_error
531
+ end
532
+
533
+ key = key.to_sym if @symbolize_names
534
+
535
+ keys << key
536
+
537
+ @s.skip(/[\t ]*/)
538
+ next if @s.skip(/\.[\t ]*/)
539
+
540
+ return keys
541
+ end
542
+ end
543
+
544
+ def parse_value
545
+ case
546
+ when @s.skip(/"/)
547
+ @s.skip(/""/) ? parse_multiline_basic_string : parse_basic_string
548
+ when @s.skip(/'/)
549
+ @s.skip(/''/) ? parse_multiline_literal_string : parse_literal_string
550
+ when len = @s.skip(/(-?\d{4})-(\d{2})-(\d{2})/)
551
+ parse_datetime(len)
552
+ when len = @s.skip(/(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/)
553
+ parse_time(len)
554
+ when val = @s.scan(/0x\h(?:_?\h)*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*/)
555
+ Integer(val)
556
+ when val1 = @s.scan(/[+\-]?(?:0|[1-9](?:_?[0-9])*)/)
557
+ val2 = @s.scan(/\.[0-9](?:_?[0-9])*/)
558
+ val3 = @s.scan(/[Ee][+\-]?[0-9](?:_?[0-9])*/)
559
+ if val2 || val3
560
+ Float(val1 + (val2 || "") + (val3 || ""))
561
+ else
562
+ Integer(val1)
563
+ end
564
+ when @s.skip(/true\b/)
565
+ true
566
+ when @s.skip(/false\b/)
567
+ false
568
+ when @s.skip(/\[/)
569
+ parse_array
570
+ when @s.skip(/\{/)
571
+ parse_inline_table
572
+ when val = @s.scan(/([+\-])?(?:(inf)|(nan))\b/)
573
+ @s[2] ? @s[1] == "-" ? -Float::INFINITY : Float::INFINITY : Float::NAN
574
+ else
575
+ unexpected_error
576
+ end
577
+ end
578
+
579
+ # object builder
580
+
581
+ class Node # :nodoc:
582
+ # This is an internal data structure to create a Ruby object from TOML.
583
+ # A node corresponds to a table in TOML.
584
+ #
585
+ # There are five node types:
586
+ #
587
+ # 1. declared
588
+ #
589
+ # Declared as a table by a dotted-key header, but not defined yet.
590
+ # This type may be changed to "1. defined_by_header".
591
+ #
592
+ # Example 1: "a" and "a.b" of "[a.b.c]"
593
+ # Example 2: "a" and "a.b" of "[[a.b.c]]".
594
+ #
595
+ # 2. defined_by_header
596
+ #
597
+ # Defined as a table by a header. This type is final.
598
+ #
599
+ # Example: "a.b.c" of "[a.b.c]"
600
+ #
601
+ # 3. defined_by_dot
602
+ #
603
+ # Defined as a table by a dotted-key value definition.
604
+ # This type is final.
605
+ #
606
+ # Example: "a" and "a.b" of "a.b.c = val"
607
+ #
608
+ # Note: we need to distinguish between defined_by_header and
609
+ # defined_by_dot because defined_by_dot can modify a table of
610
+ # defined_by_dot:
611
+ #
612
+ # a.b.c=1 # define "a.b" as defined_by_dot
613
+ # a.b.d=2 # able to modify "a.b" (add "d" to "a.b")
614
+ #
615
+ # but cannot modify a table of defined_by_header:
616
+ #
617
+ # [a.b] # define "a.b" as defined_by_header
618
+ # c=1
619
+ # [a]
620
+ # b.d=2 # unable to modify "a.b"
621
+ #
622
+ # 4. defined_as_array
623
+ #
624
+ # Defined as an array of tables. This type is final, but this node
625
+ # may be replaced with a new element of the array. A node has a
626
+ # reference to the last table in the array.
627
+ #
628
+ # Example: "a.b.c" of "[[a.b.c]]"
629
+ #
630
+ # 5. defined_as_value
631
+ #
632
+ # Defined as a value. This type is final.
633
+ #
634
+ # Example: "a.b.c" of "a.b.c = val"
635
+ def initialize(type, parent)
636
+ @type = type
637
+ @children = {}
638
+ @table = {}
639
+ @parent = parent
640
+ end
641
+
642
+ attr_accessor :type
643
+ attr_reader :children, :table
644
+
645
+ Terminal = Node.new(:defined_as_value, nil)
646
+
647
+ def path
648
+ return [] unless @parent
649
+ key, = @parent.children.find {|key, child| child == self }
650
+ @parent.path + [key]
651
+ end
652
+ end
653
+
654
+ def extend_node(node, key, type)
655
+ new_node = Node.new(type, node)
656
+ node.table[key] = new_node.table
657
+ node.children[key] = new_node
658
+ end
659
+
660
+ # handle "a.b" part of "[a.b.c]" or "[[a.b.c]]"
661
+ def declare_tables(keys)
662
+ node = @root_node
663
+ keys.each_with_index do |key, i|
664
+ child_node = node.children[key]
665
+ if child_node
666
+ node = child_node
667
+ redefine_key_error(keys[0, i], key) if node.type == :defined_as_value
668
+ else
669
+ node = extend_node(node, key, :declared)
670
+ end
671
+ end
672
+ node
673
+ end
674
+
675
+ # handle "a.b.c" part of "[a.b.c]"
676
+ def define_table(node, key)
677
+ child_node = node.children[key]
678
+ if child_node
679
+ redefine_key_error(node.path, key) if child_node.type != :declared
680
+ child_node.type = :defined_by_header
681
+ child_node
682
+ else
683
+ extend_node(node, key, :defined_by_header)
684
+ end
685
+ end
686
+
687
+ # handle "a.b.c" part of "[[a.b.c]]"
688
+ def define_array(node, key)
689
+ new_node = Node.new(:defined_as_array, node)
690
+ child_node = node.children[key]
691
+ if child_node
692
+ redefine_key_error(node.path, key) if child_node.type != :defined_as_array
693
+ node.table[key] << new_node.table
694
+ else
695
+ node.table[key] = [new_node.table]
696
+ end
697
+ node.children[key] = new_node
698
+ end
699
+
700
+ # handle "a.b.c = val"
701
+ def define_value(node, keys)
702
+ if keys.size >= 2
703
+ *keys, last_key = keys
704
+ keys.each_with_index do |key, i|
705
+ child_node = node.children[key]
706
+ if child_node
707
+ redefine_key_error(node.path, key) if child_node.type != :defined_by_dot
708
+ node = child_node
709
+ else
710
+ node = extend_node(node, key, :defined_by_dot)
711
+ end
712
+ end
713
+ else
714
+ last_key = keys.first
715
+ end
716
+ redefine_key_error(node.path, last_key) if node.children[last_key]
717
+ node.table[last_key] = parse_value
718
+ node.children[last_key] = Node::Terminal
719
+ end
720
+
721
+ def parse_toml
722
+ while true
723
+ skip_spaces
724
+
725
+ break if @s.eos?
726
+
727
+ case @s.skip(/\[\[?/)
728
+ when 1
729
+ @keys_start_pos = @s.pos - 1
730
+ @s.skip(/[\t ]*/)
731
+ *keys, last_key = parse_keys
732
+ unexpected_error unless @s.skip(/\]/)
733
+ skip_rest_of_line
734
+ @topic_node = define_table(declare_tables(keys), last_key)
735
+
736
+ when 2
737
+ @keys_start_pos = @s.pos - 2
738
+ @s.skip(/[\t ]*/)
739
+ *keys, last_key = parse_keys
740
+ unexpected_error unless @s.skip(/\]\]/)
741
+ skip_rest_of_line
742
+ @topic_node = define_array(declare_tables(keys), last_key)
743
+
744
+ else
745
+ @keys_start_pos = @s.pos
746
+ keys = parse_keys
747
+ unexpected_error unless @s.skip(/=[\t ]*/)
748
+ define_value(@topic_node, keys)
749
+ skip_rest_of_line
750
+ end
751
+ end
752
+
753
+ @root_node.table
754
+ end
755
+ end
756
+
757
+ class Generator # :nodoc:
758
+ def initialize(obj, out, sort_keys: false, use_literal_string: false, use_multiline_string: false, use_dot: false)
759
+ @obj = obj.to_hash
760
+ @out = out
761
+ @first_output = true
762
+
763
+ @sort_keys = sort_keys
764
+ @use_literal_string = use_literal_string
765
+ @use_multiline_string = use_multiline_string
766
+ @use_dot = use_dot
767
+ end
768
+
769
+ def generate
770
+ generate_hash(@obj, "", false)
771
+ @out
772
+ end
773
+
774
+ def self.escape_key(key)
775
+ new({}, "").send(:escape_key, key)
776
+ end
777
+
778
+ def self.escape_basic_string(str)
779
+ str = str.gsub(/["\\\x00-\x08\x0a-\x1f\x7f]/) do
780
+ c = ESCAPE_CHARS[$&]
781
+ c ? "\\" + c : "\\u%04x" % $&.ord
782
+ end
783
+ "\"#{ str }\""
784
+ end
785
+
786
+ private
787
+
788
+ def escape_key(key)
789
+ key = key.to_s
790
+ if key =~ /\A[A-Za-z0-9_\-]+\z/
791
+ key
792
+ else
793
+ escape_string(key)
794
+ end
795
+ end
796
+
797
+ ESCAPE_CHARS = {
798
+ ?\b => ?b, ?\t => ?t, ?\n => ?n, ?\f => ?f, ?\r => ?r, ?" => ?", ?\\ => ?\\
799
+ }
800
+
801
+ def escape_multiline_string(str)
802
+ if @use_literal_string && str =~ /\A'{0,2}(?:[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f']+(?:''?|\z))*\z/
803
+ "'''\n#{ str }'''"
804
+ else
805
+ str = str.gsub(/(""")|([\\\x00-\x08\x0b\x0c\x0e-\x1f\x7f])/) do
806
+ $1 ? '\\"""' : "\\u%04x" % $2.ord
807
+ end
808
+ "\"\"\"\n#{ str }\"\"\""
809
+ end
810
+ end
811
+
812
+ def escape_string(str)
813
+ if @use_literal_string && str =~ /\A[^\x00-\x08\x0a-\x1f\x7f']*\z/
814
+ "'#{ str }'"
815
+ else
816
+ Generator.escape_basic_string(str)
817
+ end
818
+ end
819
+
820
+ def generate_hash(hash, path, array_type)
821
+ values = []
822
+ children = []
823
+ dup_check = {}
824
+
825
+ hash.each do |key, val|
826
+ k = key.to_s
827
+ if dup_check[k]
828
+ raise ArgumentError, "duplicated key: %p and %p" % [dup_check[k], key]
829
+ end
830
+ dup_check[k] = key
831
+
832
+ k = escape_key(k)
833
+
834
+ tbl = Hash.try_convert(val)
835
+ if tbl
836
+ k2, val = dot_usable(tbl) if @use_dot
837
+ if k2
838
+ values << [k + "." + k2, val]
839
+ else
840
+ children << [:table, k, tbl]
841
+ end
842
+ else
843
+ ary = Array.try_convert(val)
844
+ ary = ary&.map do |v|
845
+ v = Hash.try_convert(v)
846
+ break unless v
847
+ v
848
+ end
849
+ if ary
850
+ children << [:array, k, ary]
851
+ else
852
+ values << [k, val]
853
+ end
854
+ end
855
+ end
856
+
857
+ if !path.empty? && (!values.empty? || hash.empty?) || array_type
858
+ @out << "\n" unless @first_output
859
+ if array_type
860
+ @out << "[[" << path << "]]\n"
861
+ else
862
+ @out << "[" << path << "]\n"
863
+ end
864
+ @first_output = false
865
+ end
866
+
867
+ unless values.empty?
868
+ values = values.sort if @sort_keys
869
+ values.each do |key, val|
870
+ @out << key << " = "
871
+ generate_value(val)
872
+ @out << "\n"
873
+ end
874
+ @first_output = false
875
+ end
876
+
877
+ unless children.empty?
878
+ children = children.sort if @sort_keys
879
+ children.each do |type, key, val|
880
+ path2 = path.empty? ? key : path + "." + key
881
+ if type == :table
882
+ generate_hash(val, path2, false)
883
+ else
884
+ val.each do |hash|
885
+ generate_hash(hash, path2, true)
886
+ end
887
+ end
888
+ end
889
+ end
890
+ end
891
+
892
+ def dot_usable(tbl)
893
+ return nil if tbl.size != 1
894
+ key, val = tbl.first
895
+ case val
896
+ when Integer, true, false, Float, Time, String
897
+ [escape_key(key), val]
898
+ when Hash
899
+ path, val = dot_usable(val)
900
+ if path
901
+ [escape_key(key) + "." + path, val]
902
+ else
903
+ nil
904
+ end
905
+ else
906
+ nil
907
+ end
908
+ end
909
+
910
+ def generate_value(val)
911
+ case val
912
+ when Integer, true, false
913
+ @out << val.to_s
914
+ when Float
915
+ case
916
+ when val.infinite?
917
+ @out << (val > 0 ? "inf" : "-inf")
918
+ when val.nan?
919
+ @out << "nan"
920
+ else
921
+ @out << val.to_s
922
+ end
923
+ when Time
924
+ @out << val.strftime("%Y-%m-%dT%H:%M:%S")
925
+ @out << val.strftime(".%N") if val != val.floor
926
+ @out << (val.utc? ? "Z" : val.strftime("%:z"))
927
+ when String
928
+ if @use_multiline_string && val.include?("\n")
929
+ @out << escape_multiline_string(val)
930
+ else
931
+ @out << escape_string(val)
932
+ end
933
+ when Array
934
+ @out << "["
935
+ first = true
936
+ val.each do |v|
937
+ @out << ", " unless first
938
+ generate_value(v)
939
+ first = false
940
+ end
941
+ @out << "]"
942
+ when Hash
943
+ if val.empty?
944
+ @out << "{}"
945
+ else
946
+ @out << "{ "
947
+ first = true
948
+ val.each do |k, v|
949
+ @out << ", " unless first
950
+ @out << escape_key(k) << " = "
951
+ generate_value(v)
952
+ first = false
953
+ end
954
+ @out << " }"
955
+ end
956
+ else
957
+ @out << val.to_inline_toml
958
+ end
959
+ end
960
+ end
961
+ end
@@ -0,0 +1,28 @@
1
+ module PerfectTOML
2
+ VERSION: String
3
+
4
+ def self.parse: (String toml_src, symbolize_names: bool) -> untyped
5
+ def self.load_file: (String filename, symbolize_names: bool) -> untyped
6
+ | (IO io, symbolize_names: bool) -> untyped
7
+ def self.generate: (untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> String
8
+ def self.save_file: (String filename, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> void
9
+ | (IO io, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> void
10
+
11
+ class LocalDateTime
12
+ def initialize: (untyped year, untyped month, untyped day, untyped hour, untyped min, untyped sec) -> void
13
+ def to_time: (?String zone) -> Time
14
+ def to_inline_toml: -> String
15
+ end
16
+
17
+ class LocalDate
18
+ def initialize: (untyped year, untyped month, untyped day) -> void
19
+ def to_time: (?String zone) -> Time
20
+ def to_inline_toml: -> String
21
+ end
22
+
23
+ class LocalTime
24
+ def initialize: (untyped hour, untyped min, untyped sec) -> void
25
+ def to_time: (?String zone) -> Time
26
+ def to_inline_toml: -> String
27
+ end
28
+ end
data/tool/decoder.rb ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/perfect_toml"
4
+ require "json"
5
+
6
+ def convert(toml)
7
+ case toml
8
+ when Hash then toml.to_h {|k, v| [k, convert(v)] }
9
+ when Array then toml.map {|v| convert(v) }
10
+ when String then { "type" => "string", "value" => toml }
11
+ when Integer then { "type" => "integer", "value" => toml.to_s }
12
+ when Float
13
+ str = toml.nan? ? "nan" : toml.infinite? ? "#{ toml > 0 ? "+" : "-" }inf" : toml.to_s
14
+ str = str.sub(/\.0\z/, "")
15
+ { "type" => "float", "value" => str }
16
+ when true then { "type" => "bool", "value" => "true" }
17
+ when false then { "type" => "bool", "value" => "false" }
18
+ when Time
19
+ str = toml.strftime("%Y-%m-%dT%H:%M:%S.%4N")
20
+ str = str.sub(/\.0000\z/, "")
21
+ zone = toml.strftime("%:z")
22
+ str << (zone == "+00:00" ? "Z" : zone)
23
+ { "type" => "datetime", "value" => str }
24
+ when PerfectTOML::LocalDateTime
25
+ { "type" => "datetime-local", "value" => toml.to_s }
26
+ when PerfectTOML::LocalDate
27
+ { "type" => "date-local", "value" => toml.to_s }
28
+ when PerfectTOML::LocalTime
29
+ { "type" => "time-local", "value" => toml.to_s }
30
+ else
31
+ raise "unknown type: %p" % toml
32
+ end
33
+ end
34
+
35
+ puts JSON.generate(convert(PerfectTOML.parse($stdin.read)))
data/tool/encoder.rb ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/perfect_toml"
4
+ require "json"
5
+ require "time"
6
+
7
+ def convert(json)
8
+ return json.map {|v| convert(v) } if Array === json
9
+ if json.key?("type")
10
+ type, val = json["type"], json["value"]
11
+ case type
12
+ when "integer" then val.to_i
13
+ when "float"
14
+ case val.downcase
15
+ when "inf", "+inf" then Float::INFINITY
16
+ when "-inf" then -Float::INFINITY
17
+ when "nan" then -Float::NAN
18
+ else
19
+ val.to_f
20
+ end
21
+ when "string" then val
22
+ when "bool" then val == "true"
23
+ when "datetime" then Time.iso8601(val)
24
+ when "datetime-local"
25
+ raise if val !~ /\A(-?\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)\z/
26
+ PerfectTOML::LocalDateTime.new($1, $2, $3, $4, $5, $6)
27
+ when "date-local"
28
+ raise if val !~ /\A(-?\d{4})-(\d{2})-(\d{2})\z/
29
+ PerfectTOML::LocalDate.new($1, $2, $3)
30
+ when "time-local"
31
+ raise if val !~ /\A(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)\z/
32
+ PerfectTOML::LocalTime.new($1, $2, $3)
33
+ else
34
+ json.to_h {|k, v| [k, convert(v)] }
35
+ end
36
+ else
37
+ json.to_h {|k, v| [k, convert(v)] }
38
+ end
39
+ end
40
+
41
+ opts = {
42
+ use_dot: ENV["TOML_ENCODER_USE_DOT"] == "1",
43
+ sort_keys: ENV["TOML_ENCODER_SORT_KEYS"] == "1",
44
+ use_literal_string: ENV["TOML_ENCODER_USE_LITERAL_STRING"] == "1",
45
+ use_multiline_string: ENV["TOML_ENCODER_USE_MULTILINE_STRING"] == "1",
46
+ }
47
+ puts PerfectTOML.generate(convert(JSON.parse($stdin.read)), **opts)
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perfect_toml24
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.1
5
+ platform: ruby
6
+ authors:
7
+ - Geckoboard
8
+ - Yusuke Endoh
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2025-04-08 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: |
15
+ PerfectTOML is yet another TOML parser.
16
+ It is fully compliant with TOML v1.0.0, and faster than existing TOML parsers for Ruby.
17
+ email:
18
+ - devs@geckoboard.com
19
+ - mame@ruby-lang.org
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - CHANGELOG.md
25
+ - Gemfile
26
+ - LICENSE
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - lib/perfect_toml.rb
31
+ - sig/perfect_toml.rbs
32
+ - tool/decoder.rb
33
+ - tool/encoder.rb
34
+ homepage: https://github.com/geckoboard/perfect_toml
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/geckoboard/perfect_toml
39
+ source_code_uri: https://github.com/geckoboard/perfect_toml
40
+ changelog_uri: https://github.com/geckoboard/perfect_toml/blob/main/CHANGELOG.md
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.4.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.0.3.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: A fast TOML parser gem fully compliant with TOML v1.0.0
60
+ test_files: []