perfect_toml 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4bdd721add5b9cda6201f1168e544ef143fe49057caadca19f84ce2415ef6511
4
+ data.tar.gz: 96cd13ab0484dbf0e2491441149e9df9d8312f16b64676ebc132610f387c7d28
5
+ SHA512:
6
+ metadata.gz: c3b46cd8667c04e06e41c9b8fc9f9c902a7ffc8f79ed07d40ba187d845702062a78eed671ed3062b8cdb5ea67cd3e011af5caf60ed18c46f275364783dbf350f
7
+ data.tar.gz: dd618bfdd46be20cf703c79422e5f11f989ccc0dd8558ccd40e51ef26c037f3a0612acf22b8d7878417c74731c04d58f2268917b716e8911358f076004a9b03e
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # PerfectTOML Changelog
2
+
3
+ ## 0.9.0 / 2022-07-17
4
+
5
+ * First release.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
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"
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 [BurntSushi/toml-test](https://github.com/BurntSushi/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
+ toml = 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.0"
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..] 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..] 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,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/perfect_toml"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "perfect_toml"
7
+ spec.version = PerfectTOML::VERSION
8
+ spec.authors = ["Yusuke Endoh"]
9
+ spec.email = ["mame@ruby-lang.org"]
10
+
11
+ spec.summary = "A fast TOML parser gem fully compliant with TOML v1.0.0"
12
+ spec.description = <<END
13
+ PerfectTOML is yet another TOML parser.
14
+ It is fully compliant with TOML v1.0.0, and faster than existing TOML parsers for Ruby.
15
+ END
16
+ spec.homepage = "https://github.com/mame/perfect_toml"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = ">= 2.7.0"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/mame/perfect_toml"
22
+ spec.metadata["changelog_uri"] = "https://github.com/mame/perfect_toml/blob/main/CHANGELOG.md"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+ 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: (
8
+ def self.save_file: (String filename, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> String) -> void
9
+ | (IO io, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> String) -> 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
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
+ raise "unknown type: %p" % type
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,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perfect_toml
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Yusuke Endoh
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ PerfectTOML is yet another TOML parser.
15
+ It is fully compliant with TOML v1.0.0, and faster than existing TOML parsers for Ruby.
16
+ email:
17
+ - mame@ruby-lang.org
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - Gemfile
24
+ - LICENSE
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - lib/perfect_toml.rb
29
+ - perfect_toml.gemspec
30
+ - sig/perfect_toml.rbs
31
+ - tool/decoder.rb
32
+ - tool/encoder.rb
33
+ homepage: https://github.com/mame/perfect_toml
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/mame/perfect_toml
38
+ source_code_uri: https://github.com/mame/perfect_toml
39
+ changelog_uri: https://github.com/mame/perfect_toml/blob/main/CHANGELOG.md
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.7.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.3.7
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: A fast TOML parser gem fully compliant with TOML v1.0.0
59
+ test_files: []