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 +7 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/Rakefile +46 -0
- data/lib/perfect_toml.rb +961 -0
- data/sig/perfect_toml.rbs +28 -0
- data/tool/decoder.rb +35 -0
- data/tool/encoder.rb +47 -0
- metadata +60 -0
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
data/Gemfile
ADDED
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
|
data/lib/perfect_toml.rb
ADDED
@@ -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: []
|