perfect_toml_improved 0.9.1

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: 7f729d5bc0925251b2f49ef5dbdf7b2c9ff17d3eac991af19725c8377af781ab
4
+ data.tar.gz: 361ceade04a8fee6241dce5e06c6a818eb1827071cc5b0b248b77bf0693b19a0
5
+ SHA512:
6
+ metadata.gz: 4ad127779efc873dec3643b056b07d5f1920ec62c2c62fcee31073a9693dff7b2c9dd204d4bbc9cf1edcf7604ea1aeba26995f37cd1ff22e7a2dd510dfef5d90
7
+ data.tar.gz: a40b95fdf1571b6d56ce852e46300d39532d8461ec1cf6e4f40fc182fa7b42e8e9a55666ade016143e5977738befc390972eb8f229d66fb75da84f477411094c
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 [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,966 @@
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..] 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, indent: 0)
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
+ @indent = indent
768
+ end
769
+
770
+ def generate
771
+ generate_hash(@obj, "", false, 0)
772
+ @out
773
+ end
774
+
775
+ def self.escape_key(key)
776
+ new({}, "").send(:escape_key, key)
777
+ end
778
+
779
+ def self.escape_basic_string(str)
780
+ str = str.gsub(/["\\\x00-\x08\x0a-\x1f\x7f]/) do
781
+ c = ESCAPE_CHARS[$&]
782
+ c ? "\\" + c : "\\u%04x" % $&.ord
783
+ end
784
+ "\"#{ str }\""
785
+ end
786
+
787
+ private
788
+
789
+ def escape_key(key)
790
+ key = key.to_s
791
+ if key =~ /\A[A-Za-z0-9_\-]+\z/
792
+ key
793
+ else
794
+ escape_string(key)
795
+ end
796
+ end
797
+
798
+ ESCAPE_CHARS = {
799
+ ?\b => ?b, ?\t => ?t, ?\n => ?n, ?\f => ?f, ?\r => ?r, ?" => ?", ?\\ => ?\\
800
+ }
801
+
802
+ def escape_multiline_string(str)
803
+ if @use_literal_string && str =~ /\A'{0,2}(?:[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f']+(?:''?|\z))*\z/
804
+ "'''\n#{ str }'''"
805
+ else
806
+ str = str.gsub(/(""")|([\\\x00-\x08\x0b\x0c\x0e-\x1f\x7f])/) do
807
+ $1 ? '\\"""' : "\\u%04x" % $2.ord
808
+ end
809
+ "\"\"\"\n#{ str }\"\"\""
810
+ end
811
+ end
812
+
813
+ def escape_string(str)
814
+ if @use_literal_string && str =~ /\A[^\x00-\x08\x0a-\x1f\x7f']*\z/
815
+ "'#{ str }'"
816
+ else
817
+ Generator.escape_basic_string(str)
818
+ end
819
+ end
820
+
821
+ def whitespace(level)
822
+ " " * @indent * level
823
+ end
824
+
825
+ def generate_hash(hash, path, array_type, level)
826
+ values = []
827
+ children = []
828
+ dup_check = {}
829
+
830
+ hash.each do |key, val|
831
+ k = key.to_s
832
+ if dup_check[k]
833
+ raise ArgumentError, "duplicated key: %p and %p" % [dup_check[k], key]
834
+ end
835
+ dup_check[k] = key
836
+
837
+ k = escape_key(k)
838
+
839
+ tbl = Hash.try_convert(val)
840
+ if tbl
841
+ k2, val = dot_usable(tbl) if @use_dot
842
+ if k2
843
+ values << [k + "." + k2, val]
844
+ else
845
+ children << [:table, k, tbl]
846
+ end
847
+ else
848
+ ary = Array.try_convert(val)
849
+ ary = ary&.map do |v|
850
+ v = Hash.try_convert(v)
851
+ break unless v
852
+ v
853
+ end
854
+ if ary
855
+ children << [:array, k, ary]
856
+ else
857
+ values << [k, val]
858
+ end
859
+ end
860
+ end
861
+
862
+ if !path.empty? && (!values.empty? || hash.empty?) || array_type
863
+ @out << "\n" unless @first_output
864
+ if array_type
865
+ @out << whitespace(level - 1) << "[[" << path << "]]\n"
866
+ else
867
+ @out << whitespace(level) << "[" << path << "]\n"
868
+ end
869
+ @first_output = false
870
+ end
871
+
872
+ unless values.empty?
873
+ values = values.sort if @sort_keys
874
+ values.each do |key, val|
875
+ @out << whitespace(level + 1) << key << " = "
876
+ generate_value(val)
877
+ @out << "\n"
878
+ end
879
+ @first_output = false
880
+ end
881
+
882
+ unless children.empty?
883
+ children = children.sort if @sort_keys
884
+ children.each do |type, key, val|
885
+ path2 = path.empty? ? key : path + "." + key
886
+ if type == :table
887
+ generate_hash(val, path2, false, level)
888
+ else
889
+ val.each do |hash|
890
+ generate_hash(hash, path2, true, level + 1)
891
+ end
892
+ end
893
+ end
894
+ end
895
+ end
896
+
897
+ def dot_usable(tbl)
898
+ return nil if tbl.size != 1
899
+ key, val = tbl.first
900
+ case val
901
+ when Integer, true, false, Float, Time, String
902
+ [escape_key(key), val]
903
+ when Hash
904
+ path, val = dot_usable(val)
905
+ if path
906
+ [escape_key(key) + "." + path, val]
907
+ else
908
+ nil
909
+ end
910
+ else
911
+ nil
912
+ end
913
+ end
914
+
915
+ def generate_value(val)
916
+ case val
917
+ when Integer, true, false
918
+ @out << val.to_s
919
+ when Float
920
+ case
921
+ when val.infinite?
922
+ @out << (val > 0 ? "inf" : "-inf")
923
+ when val.nan?
924
+ @out << "nan"
925
+ else
926
+ @out << val.to_s
927
+ end
928
+ when Time
929
+ @out << val.strftime("%Y-%m-%dT%H:%M:%S")
930
+ @out << val.strftime(".%N") if val != val.floor
931
+ @out << (val.utc? ? "Z" : val.strftime("%:z"))
932
+ when String
933
+ if @use_multiline_string && val.include?("\n")
934
+ @out << escape_multiline_string(val)
935
+ else
936
+ @out << escape_string(val)
937
+ end
938
+ when Array
939
+ @out << "["
940
+ first = true
941
+ val.each do |v|
942
+ @out << ", " unless first
943
+ generate_value(v)
944
+ first = false
945
+ end
946
+ @out << "]"
947
+ when Hash
948
+ if val.empty?
949
+ @out << "{}"
950
+ else
951
+ @out << "{ "
952
+ first = true
953
+ val.each do |k, v|
954
+ @out << ", " unless first
955
+ @out << escape_key(k) << " = "
956
+ generate_value(v)
957
+ first = false
958
+ end
959
+ @out << " }"
960
+ end
961
+ else
962
+ @out << val.to_inline_toml
963
+ end
964
+ end
965
+ end
966
+ 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_toml_improved
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.1
5
+ platform: ruby
6
+ authors:
7
+ - Yusuke Endoh
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-02-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: "PerfectTOML is yet another TOML parser.\nIt is fully compliant with
14
+ TOML v1.0.0, and faster than existing TOML parsers for Ruby.\nPerfectTomlImproved
15
+ is just a fork of PerfectToml with a features I have merge requests open for in
16
+ PerfectToml. \nPerfectTomlImproved will go away once the changes are merged. You
17
+ should use PerfectToml unless you want those features.\n"
18
+ email:
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/bemehiser/perfect_toml
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/bemehiser/perfect_toml
39
+ source_code_uri: https://github.com/bemehiser/perfect_toml
40
+ changelog_uri: https://github.com/bemehiser/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.7.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.1.6
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: A fast TOML parser gem fully compliant with TOML v1.0.0
60
+ test_files: []