pax_global_header00006660000000000000000000000064142650120240014506gustar00rootroot0000000000000052 comment=842c16fde28d3cf90e492392249bc5e327f4e37c perfect_toml-0.9.0/000077500000000000000000000000001426501202400141775ustar00rootroot00000000000000perfect_toml-0.9.0/.github/000077500000000000000000000000001426501202400155375ustar00rootroot00000000000000perfect_toml-0.9.0/.github/workflows/000077500000000000000000000000001426501202400175745ustar00rootroot00000000000000perfect_toml-0.9.0/.github/workflows/test.yml000066400000000000000000000010231426501202400212720ustar00rootroot00000000000000name: test on: [push,pull_request,workflow_dispatch] jobs: build: strategy: fail-fast: false matrix: ruby-version: [2.7, 3.0, 3.1, head] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: true - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - name: Bundle install run: | bundle install - name: Run the test suite run: | bundle exec rake TESTOPT=-v perfect_toml-0.9.0/.gitignore000066400000000000000000000001501426501202400161630ustar00rootroot00000000000000/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /toml-test-v1.2.0-linux-amd64 perfect_toml-0.9.0/CHANGELOG.md000066400000000000000000000001011426501202400160000ustar00rootroot00000000000000# PerfectTOML Changelog ## 0.9.0 / 2022-07-17 * First release. perfect_toml-0.9.0/Gemfile000066400000000000000000000001671426501202400154760ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem "rake", "~> 13.0" gem "test-unit", "~> 3.0" gem "simplecov", "~> 0.21.2" perfect_toml-0.9.0/LICENSE000066400000000000000000000020551426501202400152060ustar00rootroot00000000000000MIT License Copyright (c) 2022 Yusuke Endoh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. perfect_toml-0.9.0/LICENSE.txt000066400000000000000000000020671426501202400160270ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2022 Yusuke Endoh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. perfect_toml-0.9.0/README.md000066400000000000000000000054641426501202400154670ustar00rootroot00000000000000# PerfectTOML Yet another [TOML](https://github.com/toml-lang/toml) parser and generator. Features: * 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). * Faster than existing TOML parsers for Ruby. See [Benchmark](#benchmark). * Single-file, plain old Ruby script without any dependencies: [perfect_toml.rb](https://github.com/mame/perfect_toml/blob/master/lib/perfect_toml.rb). ## Installation Install the gem and add to the application's Gemfile by executing: $ bundle add perfect_toml If bundler is not being used to manage dependencies, install the gem by executing: $ gem install perfect_toml ## Parser Usage ```ruby require "perfect_toml" # Decodes a TOML string p PerfectTOML.parse("key = 42") #=> { "key" => 42 } # Load a TOML file PerfectTOML.load_file("file.toml") # If you want Symbol keys: PerfectTOML.load_file("file.toml", symbolize_names: true) ``` ## Generator Usage ```ruby require "perfect_toml" # Encode a Hash in TOML format p PerfectTOML.generate({ key: 42 }) #=> "key = 42\n" # Save a Hash in a TOML file PerfectTOML.save_file("file.toml", { key: 42 }) ``` See the document for options. ## TOML's value vs. Ruby's value TOML's table is converted to Ruby's Hash, and vice versa. Other most TOML values are converted to an object of Ruby class of the same name: for example, TOML's String corresponds to Ruby's String. Because there are no classes corresponding to TOML's Local Date-Time, Local Date, and Local Time, PerfectTOML provides dedicated classes, respectively, `PerfectTOML::LocalDateTime`, `PerfectTOML::LocalDate`, and `PerfectTOML::LocalTime`. ```ruby require "perfect_toml" p PerfectTOML.parse("local-date = 1970-01-01) #=> { "local-date" => # } ``` ## Benchmark PerfectTOML is 5x faster than [tomlrb](https://github.com/fbernier/tomlrb), and 100x faster than [toml-rb](https://github.com/emancu/toml-rb). ```ruby require "benchmark/ips" require_relative "lib/perfect_toml" require "toml-rb" require "tomlrb" # https://raw.githubusercontent.com/toml-lang/toml/v0.5.0/examples/example-v0.4.0.toml toml = File.read("example-v0.4.0.toml") Benchmark.ips do |x| x.report("emancu/toml-rb") { TomlRB.parse(data) } x.report("fbernier/tomlrb") { Tomlrb.parse(data) } x.report("mame/perfect_toml") { PerfectTOML.parse(data) } x.compare! end ``` ``` ... Comparison: mame/perfect_toml: 2982.5 i/s fbernier/tomlrb: 515.7 i/s - 5.78x (± 0.00) slower emancu/toml-rb: 25.4 i/s - 117.36x (± 0.00) slower ``` ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/mame/perfect_toml. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). perfect_toml-0.9.0/Rakefile000066400000000000000000000025051426501202400156460ustar00rootroot00000000000000require "bundler/gem_tasks" require "rake/testtask" require "rdoc/task" Rake::TestTask.new(:core_test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end TOML_TEST = "./toml-test-v1.2.0-linux-amd64" file TOML_TEST do require "open-uri" require "zlib" URI.open("https://github.com/BurntSushi/toml-test/releases/download/v1.2.0/toml-test-v1.2.0-linux-amd64.gz", "rb") do |f| File.binwrite(TOML_TEST, Zlib::GzipReader.new(f).read) File.chmod(0o755, TOML_TEST) end end task :toml_decoder_test => TOML_TEST do sh "./toml-test-v1.2.0-linux-amd64", "./tool/decoder.rb" end task :toml_encoder_test => TOML_TEST do ["0000", "1000", "0010", "0001", "0011"].each do |mode| ENV["TOML_ENCODER_USE_DOT"] = mode[0] ENV["TOML_ENCODER_SORT_KEYS"] = mode[1] ENV["TOML_ENCODER_USE_LITERAL_STRING"] = mode[2] ENV["TOML_ENCODER_USE_MULTILINE_STRING"] = mode[3] sh "./toml-test-v1.2.0-linux-amd64", "./tool/encoder.rb", "--encoder", "-skip", "valid/string/multiline-quotes" end end task :test => [:core_test, :toml_decoder_test, :toml_encoder_test] task default: :test Rake::RDocTask.new do |rdoc| files =["README.md", "LICENSE", "lib/perfect_toml.rb"] rdoc.rdoc_files.add(files) rdoc.main = "README.md" rdoc.title = "PerfectTOML Docs" rdoc.rdoc_dir = "doc/rdoc" end perfect_toml-0.9.0/lib/000077500000000000000000000000001426501202400147455ustar00rootroot00000000000000perfect_toml-0.9.0/lib/perfect_toml.rb000066400000000000000000000631231426501202400177620ustar00rootroot00000000000000# MIT License # # Copyright (c) 2022 Yusuke Endoh # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. require "strscan" module PerfectTOML VERSION = "0.9.0" class LocalDateTimeBase def to_inline_toml to_s end def inspect "#<#{ self.class } #{ to_s }>" end def pretty_print(q) q.text inspect end def ==(other) self.class == other.class && [:year, :month, :day, :hour, :min, :sec].all? do |key| !respond_to?(key) || (send(key) == other.send(key)) end end include Comparable def <=>(other) return nil if self.class != other.class [:year, :month, :day, :hour, :min, :sec].each do |key| next unless respond_to?(key) cmp = send(key) <=> other.send(key) return cmp if cmp != 0 end return 0 end end # Represents TOML's Local Date-Time # # See https://toml.io/en/v1.0.0#local-date-time class LocalDateTime < LocalDateTimeBase def initialize(year, month, day, hour, min, sec) @year = year.to_i @month = month.to_i @day = day.to_i @hour = hour.to_i @min = min.to_i @sec = Numeric === sec ? sec : sec.include?(".") ? Rational(sec) : sec.to_i end attr_reader :year, :month, :day, :hour, :min, :sec # Converts to a Time object with the local timezone. # # ldt = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4) # ldt.to_time #=> 1970-01-01 02:03:04 +0900 # # You can specify timezone by passing an argument. # # ldt.to_time("UTC") #=> 1970-01-01 02:03:04 UTC # ldt.to_time("+00:00") #=> 1970-01-01 02:03:04 +0000 def to_time(zone = nil) @time = Time.new(@year, @month, @day, @hour, @min, @sec, zone) end # Returns a string representation in RFC 3339 format # # ldt = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4) # ldt.to_s #=> 1970-01-01T02:03:04 def to_s s = "%04d-%02d-%02dT%02d:%02d:%02d" % [@year, @month, @day, @hour, @min, @sec] s << ("%11.9f" % (@sec - @sec.floor))[1..] unless Integer === @sec s end end # Represents TOML's Local Date # # See https://toml.io/en/v1.0.0#local-date class LocalDate < LocalDateTimeBase def initialize(year, month, day) @year = year.to_i @month = month.to_i @day = day.to_i end attr_reader :year, :month, :day # Converts to a Time object with the local timezone. # Its time will be 00:00:00. # # ld = PerfectTOML::LocalDate.new(1970, 1, 1) # ld.to_time #=> 1970-01-01 00:00:00 +0900 # # You can specify timezone by passing an argument. # # ld.to_time("UTC") #=> 1970-01-01 00:00:00 UTC # ld.to_time("+00:00") #=> 1970-01-01 00:00:00 +0000 def to_time(zone = nil) Time.new(@year, @month, @day, 0, 0, 0, zone) end # Returns a string representation in RFC 3339 format # # ld = PerfectTOML::LocalDate.new(1970, 1, 1) # ld.to_s #=> 1970-01-01 def to_s "%04d-%02d-%02d" % [@year, @month, @day] end end # Represents TOML's Local Time # # See https://toml.io/en/v1.0.0#local-time class LocalTime < LocalDateTimeBase def initialize(hour, min, sec) @hour = hour.to_i @min = min.to_i @sec = Numeric === sec ? sec : sec.include?(".") ? Rational(sec) : sec.to_i end attr_reader :hour, :min, :sec # Converts to a Time object with the local timezone. # You need to specify year, month, and day. # # ld = PerfectTOML::LocalTime.new(2, 3, 4) # ld.to_time(1970, 1, 1) #=> 1970-01-01 02:03:04 +0900 # # You can specify timezone by passing the fourth argument. # # ld.to_time(1970, 1, 1, "UTC") #=> 1970-01-01 02:03:04 UTC # ld.to_time(1970, 1, 1, "+00:00") #=> 1970-01-01 02:03:04 +0000 def to_time(year, month, day, zone = nil) Time.new(year, month, day, @hour, @min, @sec, zone) end # Returns a string representation in RFC 3339 format # # lt = PerfectTOML::LocalTime.new(2, 3, 4) # lt.to_s #=> 02:03:04 def to_s s = "%02d:%02d:%02d" % [@hour, @min, @sec] s << ("%11.9f" % (@sec - @sec.floor))[1..] unless Integer === @sec s end end # call-seq: # # parse(toml_src, symbolize_names: boolean) -> Object # # Decodes a TOML string. # # PerfectTOML.parse("key = 42") #=> { "key" => 42 } # # src = <<~END # [foo] # bar = "baz" # END # PerfectTOML.parse(src) #=> { "foo" => { "bar" => "baz" } } # # All keys in the Hash are String by default. # If a keyword `symbolize_names` is specficied as truthy, # all keys in the Hash will be Symbols. # # PerfectTOML.parse("key = 42", symbolize_names: true) #=> { :key => 42 } # # src = <<~END # [foo] # bar = "baz" # END # PerfectTOML.parse(src, symbolize_names: true) #=> { :key => { :bar => "baz" } } def self.parse(toml_src, **opts) Parser.new(toml_src, **opts).parse end # call-seq: # # load_file(filename, symbolize_names: boolean) -> Object # # Loads a TOML file. # # # test.toml # # key = 42 # PerfectTOML.load_file("test.toml") #=> { "key" => 42 } # # See PerfectTOML.parse for options. def self.load_file(io, **opts) io = File.open(io, encoding: "UTF-8") unless IO === io parse(io.read, **opts) end # call-seq: # # generate(hash, sort_keys: false, use_literal_string: false, use_multiline_string: false, use_dot: false) -> String # # Encode a Hash in TOML format. # # PerfectTOML.generate({ key: 42 }) # # output: # # key = 42 # # The order of hashes are respected by default. # If you want to sort them, you can use +sort_keys+ keyword: # # PerfectTOML.generate({ z: 1, a: 2 }) # # output: # # z = 1 # # a = 2 # # PerfectTOML.generate({ z: 1, a: 2 }, sort_keys: true) # # output: # # a = 2 # # z = 1 # # By default, all strings are quoted by quotation marks. # If +use_literal_string+ keyword is specified as truthy, # it prefers a literal string quoted by single quotes: # # PerfectTOML.generate({ a: "foo" }) # # output: # # a = "foo" # # PerfectTOML.generate({ a: "foo" }, use_literal_string: true) # # output: # # a = 'foo' # # Multiline strings are not used by default. # If +use_multiline_string+ keyword is specified as truthy, # it uses a multiline string if the string contains a newline. # # PerfectTOML.generate({ a: "foo\nbar" }) # # output: # # a = "foo\nbar" # # PerfectTOML.generate({ a: "foo\nbar" }, use_multiline_string: true) # # output: # # a = """ # # foo # # bar" # # By default, dotted keys are used only in a header. # If +use_dot+ keyword is specified as truthy, # it uses a dotted key only when a subtree does not branch. # # PerfectTOML.generate({ a: { b: { c: { d: 42 } } } }) # # output: # # [a.b.c] # # d = 42 # # PerfectTOML.generate({ a: { b: { c: { d: 42 } } } }, use_dot: true) # # output: # # a.b.c.d = 42 def self.generate(hash, **opts) out = "" Generator.new(hash, out, **opts).generate out end # call-seq: # # save_file(filename, hash, symbolize_names: boolean) -> Object # # Saves a Hash into a file in TOML format. # # PerfectTOML.save_file("out.toml", { key: 42 }) # # out.toml # # key = 42 # # See PerfectTOML.generate for options. def self.save_file(io, hash, **opts) io = File.open(io, mode: "w", encoding: "UTF-8") unless IO === io Generator.new(hash, io, **opts).generate ensure io.close end class ParseError < StandardError; end class Parser # :nodoc: def initialize(src, symbolize_names: false) @s = StringScanner.new(src) @symbolize_names = symbolize_names @root_node = @topic_node = Node.new(1, nil) end def parse parse_toml end private # error handling def error(msg) prev = @s.string.byteslice(0, @s.pos) last_newline = prev.rindex("\n") bol = last_newline ? prev[0, last_newline + 1].bytesize : 0 lineno = prev.count("\n") + 1 column = @s.pos - bol + 1 raise ParseError, "#{ msg } at line %d column %d" % [lineno, column] end def unterminated_string_error error "unterminated string" end def unexpected_error if @s.eos? error "unexpected end" elsif @s.scan(/[A-Za-z0-9_\-]+/) str = @s[0] @s.unscan error "unexpected identifier found: #{ str.dump }" else error "unexpected character found: #{ @s.peek(1).dump }" end end def redefine_key_error(keys, dup_key) @s.pos = @keys_start_pos keys = (keys + [dup_key]).map {|key| Generator.escape_key(key) }.join(".") error "cannot redefine `#{ keys }`" end # helpers for parsing def skip_spaces @s.skip(/(?:[\t\n ]|\r\n|#[^\x00-\x08\x0a-\x1f\x7f]*(?:\n|\r\n))+/) skip_comment if @s.check(/#/) end def skip_comment return if @s.skip(/#[^\x00-\x08\x0a-\x1f\x7f]*(?:\n|\r\n|\z)/) @s.skip(/[^\x00-\x08\x0a-\x1f\x7f]*/) unexpected_error end def skip_rest_of_line @s.skip(/[\t ]+/) case when @s.check(/#/) then skip_comment when @s.skip(/\n|\r\n/) || @s.eos? else unexpected_error end end # parsing for strings ESCAPE_CHARS = { ?b => ?\b, ?t => ?\t, ?n => ?\n, ?f => ?\f, ?r => ?\r, ?" => ?", ?\\ => ?\\ } def parse_escape_char if @s.skip(/\\/) if @s.skip(/([btnmfr"\\])|u([0-9A-Fa-f]{4})|U([0-9A-Fa-f]{8})/) @s[1] ? ESCAPE_CHARS[@s[1]] : (@s[2] || @s[3]).hex.chr("UTF-8") else unterminated_string_error if @s.eos? error "invalid escape character in string: #{ @s.peek(1).dump }" end else unterminated_string_error if @s.eos? error "invalid character in string: #{ @s.peek(1).dump }" end end def parse_basic_string str = "" while true str << @s.scan(/[^\x00-\x08\x0a-\x1f\x7f"\\]*/) return str if @s.skip(/"/) str << parse_escape_char end end def parse_multiline_basic_string # skip a newline @s.skip(/\n|\r\n/) str = "" while true str << @s.scan(/[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f"\\]*/) delimiter = @s.skip(/"{1,5}/) if delimiter str << "\"" * (delimiter % 3) return str if delimiter >= 3 next end next if @s.skip(/\\[\t ]*(?:\n|\r\n)(?:[\t\n ]|\r\n)*/) str << parse_escape_char end end def parse_literal_string str = @s.scan(/[^\x00-\x08\x0a-\x1f\x7f']*/) return str if @s.skip(/'/) unterminated_string_error if @s.eos? error "invalid character in string: #{ @s.peek(1).dump }" end def parse_multiline_literal_string # skip a newline @s.skip(/\n|\r\n/) str = "" while true str << @s.scan(/[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f']*/) if delimiter = @s.skip(/'{1,5}/) str << "'" * (delimiter % 3) return str if delimiter >= 3 next end unterminated_string_error if @s.eos? error "invalid character in string: #{ @s.peek(1).dump }" end end # parsing for date/time def parse_datetime(preread_len) str = @s[0] pos = @s.pos - preread_len year, month, day = @s[1], @s[2], @s[3] if @s.skip(/[T ](\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/i) str << @s[0] hour, min, sec = @s[1], @s[2], @s[3] raise ArgumentError unless (0..23).cover?(hour.to_i) zone = @s.scan(/(Z)|[-+]\d{2}:\d{2}/i) time = Time.new(year, month, day, hour, min, sec.to_r, @s[1] ? "UTC" : zone) if zone time else LocalDateTime.new(year, month, day, hour, min, sec) end else Time.new(year, month, day, 0, 0, 0, "Z") # parse check LocalDate.new(year, month, day) end rescue ArgumentError @s.pos = pos error "failed to parse date or datetime \"#{ str }\"" end def parse_time(preread_len) hour, min, sec = @s[1], @s[2], @s[3] Time.new(1970, 1, 1, hour, min, sec.to_r, "Z") # parse check LocalTime.new(hour, min, sec) rescue ArgumentError @s.pos -= preread_len error "failed to parse time \"#{ @s[0] }\"" end # parsing for inline array/table def parse_array ary = [] while true skip_spaces break if @s.skip(/\]/) ary << parse_value skip_spaces next if @s.skip(/,/) break if @s.skip(/\]/) unexpected_error end ary end def parse_inline_table @s.skip(/[\t ]*/) if @s.skip(/\}/) {} else tmp_node = Node.new(1, nil) while true @keys_start_pos = @s.pos keys = parse_keys @s.skip(/[\t ]*/) unexpected_error unless @s.skip(/=[\t ]*/) define_value(tmp_node, keys) @s.skip(/[\t ]*/) next if @s.skip(/,[\t ]*/) break if @s.skip(/\}/) unexpected_error end tmp_node.table end end # parsing key and value def parse_keys keys = [] while true case when key = @s.scan(/[A-Za-z0-9_\-]+/) when @s.skip(/"/) then key = parse_basic_string when @s.skip(/'/) then key = parse_literal_string else unexpected_error end key = key.to_sym if @symbolize_names keys << key @s.skip(/[\t ]*/) next if @s.skip(/\.[\t ]*/) return keys end end def parse_value case when @s.skip(/"/) @s.skip(/""/) ? parse_multiline_basic_string : parse_basic_string when @s.skip(/'/) @s.skip(/''/) ? parse_multiline_literal_string : parse_literal_string when len = @s.skip(/(-?\d{4})-(\d{2})-(\d{2})/) parse_datetime(len) when len = @s.skip(/(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/) parse_time(len) when val = @s.scan(/0x\h(?:_?\h)*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*/) Integer(val) when val1 = @s.scan(/[+\-]?(?:0|[1-9](?:_?[0-9])*)/) val2 = @s.scan(/\.[0-9](?:_?[0-9])*/) val3 = @s.scan(/[Ee][+\-]?[0-9](?:_?[0-9])*/) if val2 || val3 Float(val1 + (val2 || "") + (val3 || "")) else Integer(val1) end when @s.skip(/true\b/) true when @s.skip(/false\b/) false when @s.skip(/\[/) parse_array when @s.skip(/\{/) parse_inline_table when val = @s.scan(/([+\-])?(?:(inf)|(nan))\b/) @s[2] ? @s[1] == "-" ? -Float::INFINITY : Float::INFINITY : Float::NAN else unexpected_error end end # object builder class Node # :nodoc: # This is an internal data structure to create a Ruby object from TOML. # A node corresponds to a table in TOML. # # There are five node types: # # 1. declared # # Declared as a table by a dotted-key header, but not defined yet. # This type may be changed to "1. defined_by_header". # # Example 1: "a" and "a.b" of "[a.b.c]" # Example 2: "a" and "a.b" of "[[a.b.c]]". # # 2. defined_by_header # # Defined as a table by a header. This type is final. # # Example: "a.b.c" of "[a.b.c]" # # 3. defined_by_dot # # Defined as a table by a dotted-key value definition. # This type is final. # # Example: "a" and "a.b" of "a.b.c = val" # # Note: we need to distinguish between defined_by_header and # defined_by_dot because defined_by_dot can modify a table of # defined_by_dot: # # a.b.c=1 # define "a.b" as defined_by_dot # a.b.d=2 # able to modify "a.b" (add "d" to "a.b") # # but cannot modify a table of defined_by_header: # # [a.b] # define "a.b" as defined_by_header # c=1 # [a] # b.d=2 # unable to modify "a.b" # # 4. defined_as_array # # Defined as an array of tables. This type is final, but this node # may be replaced with a new element of the array. A node has a # reference to the last table in the array. # # Example: "a.b.c" of "[[a.b.c]]" # # 5. defined_as_value # # Defined as a value. This type is final. # # Example: "a.b.c" of "a.b.c = val" def initialize(type, parent) @type = type @children = {} @table = {} @parent = parent end attr_accessor :type attr_reader :children, :table Terminal = Node.new(:defined_as_value, nil) def path return [] unless @parent key, = @parent.children.find {|key, child| child == self } @parent.path + [key] end end def extend_node(node, key, type) new_node = Node.new(type, node) node.table[key] = new_node.table node.children[key] = new_node end # handle "a.b" part of "[a.b.c]" or "[[a.b.c]]" def declare_tables(keys) node = @root_node keys.each_with_index do |key, i| child_node = node.children[key] if child_node node = child_node redefine_key_error(keys[0, i], key) if node.type == :defined_as_value else node = extend_node(node, key, :declared) end end node end # handle "a.b.c" part of "[a.b.c]" def define_table(node, key) child_node = node.children[key] if child_node redefine_key_error(node.path, key) if child_node.type != :declared child_node.type = :defined_by_header child_node else extend_node(node, key, :defined_by_header) end end # handle "a.b.c" part of "[[a.b.c]]" def define_array(node, key) new_node = Node.new(:defined_as_array, node) child_node = node.children[key] if child_node redefine_key_error(node.path, key) if child_node.type != :defined_as_array node.table[key] << new_node.table else node.table[key] = [new_node.table] end node.children[key] = new_node end # handle "a.b.c = val" def define_value(node, keys) if keys.size >= 2 *keys, last_key = keys keys.each_with_index do |key, i| child_node = node.children[key] if child_node redefine_key_error(node.path, key) if child_node.type != :defined_by_dot node = child_node else node = extend_node(node, key, :defined_by_dot) end end else last_key = keys.first end redefine_key_error(node.path, last_key) if node.children[last_key] node.table[last_key] = parse_value node.children[last_key] = Node::Terminal end def parse_toml while true skip_spaces break if @s.eos? case @s.skip(/\[\[?/) when 1 @keys_start_pos = @s.pos - 1 @s.skip(/[\t ]*/) *keys, last_key = parse_keys unexpected_error unless @s.skip(/\]/) skip_rest_of_line @topic_node = define_table(declare_tables(keys), last_key) when 2 @keys_start_pos = @s.pos - 2 @s.skip(/[\t ]*/) *keys, last_key = parse_keys unexpected_error unless @s.skip(/\]\]/) skip_rest_of_line @topic_node = define_array(declare_tables(keys), last_key) else @keys_start_pos = @s.pos keys = parse_keys unexpected_error unless @s.skip(/=[\t ]*/) define_value(@topic_node, keys) skip_rest_of_line end end @root_node.table end end class Generator # :nodoc: def initialize(obj, out, sort_keys: false, use_literal_string: false, use_multiline_string: false, use_dot: false) @obj = obj.to_hash @out = out @first_output = true @sort_keys = sort_keys @use_literal_string = use_literal_string @use_multiline_string = use_multiline_string @use_dot = use_dot end def generate generate_hash(@obj, "", false) @out end def self.escape_key(key) new({}, "").send(:escape_key, key) end def self.escape_basic_string(str) str = str.gsub(/["\\\x00-\x08\x0a-\x1f\x7f]/) do c = ESCAPE_CHARS[$&] c ? "\\" + c : "\\u%04x" % $&.ord end "\"#{ str }\"" end private def escape_key(key) key = key.to_s if key =~ /\A[A-Za-z0-9_\-]+\z/ key else escape_string(key) end end ESCAPE_CHARS = { ?\b => ?b, ?\t => ?t, ?\n => ?n, ?\f => ?f, ?\r => ?r, ?" => ?", ?\\ => ?\\ } def escape_multiline_string(str) if @use_literal_string && str =~ /\A'{0,2}(?:[^\x00-\x08\x0b\x0c\x0e-\x1f\x7f']+(?:''?|\z))*\z/ "'''\n#{ str }'''" else str = str.gsub(/(""")|([\\\x00-\x08\x0b\x0c\x0e-\x1f\x7f])/) do $1 ? '\\"""' : "\\u%04x" % $2.ord end "\"\"\"\n#{ str }\"\"\"" end end def escape_string(str) if @use_literal_string && str =~ /\A[^\x00-\x08\x0a-\x1f\x7f']*\z/ "'#{ str }'" else Generator.escape_basic_string(str) end end def generate_hash(hash, path, array_type) values = [] children = [] dup_check = {} hash.each do |key, val| k = key.to_s if dup_check[k] raise ArgumentError, "duplicated key: %p and %p" % [dup_check[k], key] end dup_check[k] = key k = escape_key(k) tbl = Hash.try_convert(val) if tbl k2, val = dot_usable(tbl) if @use_dot if k2 values << [k + "." + k2, val] else children << [:table, k, tbl] end else ary = Array.try_convert(val) ary = ary&.map do |v| v = Hash.try_convert(v) break unless v v end if ary children << [:array, k, ary] else values << [k, val] end end end if !path.empty? && (!values.empty? || hash.empty?) || array_type @out << "\n" unless @first_output if array_type @out << "[[" << path << "]]\n" else @out << "[" << path << "]\n" end @first_output = false end unless values.empty? values = values.sort if @sort_keys values.each do |key, val| @out << key << " = " generate_value(val) @out << "\n" end @first_output = false end unless children.empty? children = children.sort if @sort_keys children.each do |type, key, val| path2 = path.empty? ? key : path + "." + key if type == :table generate_hash(val, path2, false) else val.each do |hash| generate_hash(hash, path2, true) end end end end end def dot_usable(tbl) return nil if tbl.size != 1 key, val = tbl.first case val when Integer, true, false, Float, Time, String [escape_key(key), val] when Hash path, val = dot_usable(val) if path [escape_key(key) + "." + path, val] else nil end else nil end end def generate_value(val) case val when Integer, true, false @out << val.to_s when Float case when val.infinite? @out << (val > 0 ? "inf" : "-inf") when val.nan? @out << "nan" else @out << val.to_s end when Time @out << val.strftime("%Y-%m-%dT%H:%M:%S") @out << val.strftime(".%N") if val != val.floor @out << (val.utc? ? "Z" : val.strftime("%:z")) when String if @use_multiline_string && val.include?("\n") @out << escape_multiline_string(val) else @out << escape_string(val) end when Array @out << "[" first = true val.each do |v| @out << ", " unless first generate_value(v) first = false end @out << "]" when Hash if val.empty? @out << "{}" else @out << "{ " first = true val.each do |k, v| @out << ", " unless first @out << escape_key(k) << " = " generate_value(v) first = false end @out << " }" end else @out << val.to_inline_toml end end end end perfect_toml-0.9.0/perfect_toml.gemspec000066400000000000000000000024461426501202400202350ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/perfect_toml" Gem::Specification.new do |spec| spec.name = "perfect_toml" spec.version = PerfectTOML::VERSION spec.authors = ["Yusuke Endoh"] spec.email = ["mame@ruby-lang.org"] spec.summary = "A fast TOML parser gem fully compliant with TOML v1.0.0" spec.description = < untyped def self.load_file: (String filename, symbolize_names: bool) -> untyped | (IO io, symbolize_names: bool) -> untyped def self.generate: ( def self.save_file: (String filename, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> String) -> void | (IO io, untyped data, sort_keys: boolean, use_literal_string: boolean, use_multiline_string: boolean, use_dot: boolean) -> String) -> void class LocalDateTime def initialize: (untyped year, untyped month, untyped day, untyped hour, untyped min, untyped sec) -> void def to_time: (?String zone) -> Time def to_inline_toml: -> String end class LocalDate def initialize: (untyped year, untyped month, untyped day) -> void def to_time: (?String zone) -> Time def to_inline_toml: -> String end class LocalTime def initialize: (untyped hour, untyped min, untyped sec) -> void def to_time: (?String zone) -> Time def to_inline_toml: -> String end end perfect_toml-0.9.0/test/000077500000000000000000000000001426501202400151565ustar00rootroot00000000000000perfect_toml-0.9.0/test/conformance_test.rb000066400000000000000000000524151426501202400210430ustar00rootroot00000000000000require "test_helper" # https://toml.io/en/v1.0.0 class ConformanceTest < Test::Unit::TestCase def test_comment exp = { "another" => "# This is not a comment", "key" => "value" } assert_equal(exp, PerfectTOML.parse(<<-'END')) # This is a full-line comment key = "value" # This is a comment at the end of a line another = "# This is not a comment" END end def test_key_value exp = { "key" => "value" } assert_equal(exp, PerfectTOML.parse(<<-'END')) key = "value" END end def test_unspecified_value assert_parse_error("unexpected character found: \"#\" at line 1 column 7") do PerfectTOML.parse(<<-'END') key = # INVALID END end end def test_no_newline_after_value assert_parse_error("unexpected identifier found: \"last\" at line 1 column 15") do PerfectTOML.parse(<<-'END') first = "Tom" last = "Preston-Werner" # INVALID END end end def test_keys exp = { "key" => "value", "bare_key" => "value", "bare-key" => "value", "1234" => "value", } assert_equal(exp, PerfectTOML.parse(<<-'END')) key = "value" bare_key = "value" bare-key = "value" 1234 = "value" END end def test_quoted_keys exp = { "127.0.0.1" => "value", "character encoding" => "value", "key2" => "value", "quoted \"value\"" => "value", "ʎǝʞ" => "value", } assert_equal(exp, PerfectTOML.parse(<<-'END')) "127.0.0.1" = "value" "character encoding" = "value" "ʎǝʞ" = "value" 'key2' = "value" 'quoted "value"' = "value" END end def test_empty_keys assert_parse_error("unexpected character found: \"=\" at line 1 column 1") do PerfectTOML.parse(<<-'END') = "no key name" # INVALID END end exp = { "" => "blank" } assert_equal(exp, PerfectTOML.parse(<<-'END')) "" = "blank" # VALID but discouraged END assert_equal(exp, PerfectTOML.parse(<<-'END')) '' = 'blank' # VALID but discouraged END end def test_dotted_keys exp = { "name" => "Orange", "physical" => { "color" => "orange", "shape" => "round", }, "site" => { "google.com" => true, }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) name = "Orange" physical.color = "orange" physical.shape = "round" site."google.com" = true END end def test_whitespace_around_dot exp = { "fruit" => { "name" => "banana", "color" => "yellow", "flavor" => "banana", } } assert_equal(exp, PerfectTOML.parse(<<-'END')) fruit.name = "banana" # this is best practice fruit. color = "yellow" # same as fruit.color fruit . flavor = "banana" # same as fruit.flavor END end def test_duplicated_keys assert_parse_error("cannot redefine `name` at line 3 column 1") do PerfectTOML.parse(<<-'END') # DO NOT DO THIS name = "Tom" name = "Pradyun" END end end def test_duplicated_keys_with_quote assert_parse_error("cannot redefine `spelling` at line 3 column 1") do PerfectTOML.parse(<<-'END') # THIS WILL NOT WORK spelling = "favorite" "spelling" = "favourite" END end end def test_parallel_dotted_keys exp = { "fruit" => { "apple" => { "smooth" => true, }, "orange" => 2, } } assert_equal(exp, PerfectTOML.parse(<<-'END')) # This makes the key "fruit" into a table. fruit.apple.smooth = true # So then you can add to the table "fruit" like so: fruit.orange = 2 END end def test_inconsistent_dotted_keys assert_parse_error("cannot redefine `fruit.apple` at line 8 column 1") do PerfectTOML.parse(<<-'END') # THE FOLLOWING IS INVALID # This defines the value of fruit.apple to be an integer. fruit.apple = 1 # But then this treats fruit.apple like it's a table. # You can't turn an integer into a table. fruit.apple.smooth = true END end end def test_dotted_keys_out_of_order exp = { "apple" => { "type" => "fruit", "skin" => "thin", "color" => "red", }, "orange" => { "type" => "fruit", "skin" => "thick", "color" => "orange", }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # VALID BUT DISCOURAGED apple.type = "fruit" orange.type = "fruit" apple.skin = "thin" orange.skin = "thick" apple.color = "red" orange.color = "orange" END end def test_pi exp = { "3" => { "14159" => "pi" } } assert_equal(exp, PerfectTOML.parse(<<-'END')) 3.14159 = "pi" END end def test_basic_string exp = { "str" => "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." } assert_equal(exp, PerfectTOML.parse(<<-'END')) str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." END end def test_multiline_basic_strings exp = { "str1" => "Roses are red\nViolets are blue", "str2" => "Roses are red\nViolets are blue", "str3" => "Roses are red\r\nViolets are blue", } assert_equal(exp, PerfectTOML.parse(<<-'END')) str1 = """ Roses are red Violets are blue""" # On a Unix system, the above multi-line string will most likely be the same as: str2 = "Roses are red\nViolets are blue" # On a Windows system, it will most likely be equivalent to: str3 = "Roses are red\r\nViolets are blue" END end def test_line_ending_backslask_in_multiline_basic_string exp = { "str1" => "The quick brown fox jumps over the lazy dog.", "str2" => "The quick brown fox jumps over the lazy dog.", "str3" => "The quick brown fox jumps over the lazy dog.", } assert_equal(exp, PerfectTOML.parse(<<-'END')) # The following strings are byte-for-byte equivalent: str1 = "The quick brown fox jumps over the lazy dog." str2 = """ The quick brown \ fox jumps over \ the lazy dog.""" str3 = """\ The quick brown \ fox jumps over \ the lazy dog.\ """ END end def test_quotation_marks_in_multiline_basic_string exp = { "str4" => 'Here are two quotation marks: "". Simple enough.', "str5" => 'Here are three quotation marks: """.', "str6" => 'Here are fifteen quotation marks: """"""""""""""".', "str7" => '"This," she said, "is just a pointless statement."', } assert_equal(exp, PerfectTOML.parse(<<-'END')) str4 = """Here are two quotation marks: "". Simple enough.""" # str5 = """Here are three quotation marks: """.""" # INVALID str5 = """Here are three quotation marks: ""\".""" str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\".""" # "This," she said, "is just a pointless statement." str7 = """"This," she said, "is just a pointless statement."""" END assert_parse_error("unexpected character found: \".\" at line 1 column 46") do PerfectTOML.parse(<<-END) str5 = """Here are three quotation marks: """.""" # INVALID END end end def test_literal_strings exp = { "winpath" => 'C:\\Users\\nodejs\\templates', "winpath2" => '\\\\ServerX\\admin$\\system32\\', "quoted" => 'Tom "Dubs" Preston-Werner', "regex" => '<\\i\\c*\\s*>', } assert_equal(exp, PerfectTOML.parse(<<-'END')) # What you see is what you get. winpath = 'C:\Users\nodejs\templates' winpath2 = '\\ServerX\admin$\system32\' quoted = 'Tom "Dubs" Preston-Werner' regex = '<\i\c*\s*>' END end def test_multiline_literal_strings exp = { "regex2" => 'I [dw]on\'t need \\d{2} apples', "lines" => "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", } assert_equal(exp, PerfectTOML.parse(<<-'END')) regex2 = '''I [dw]on't need \d{2} apples''' lines = ''' The first newline is trimmed in raw strings. All other whitespace is preserved. ''' END end def test_quotation_marks_in_multiline_literal_string exp = { "quot15" => 'Here are fifteen quotation marks: """""""""""""""', "apos15" => "Here are fifteen apostrophes: '''''''''''''''", "str" => "'That,' she said, 'is still pointless.'", } assert_equal(exp, PerfectTOML.parse(<<-'END')) quot15 = '''Here are fifteen quotation marks: """""""""""""""''' # apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID apos15 = "Here are fifteen apostrophes: '''''''''''''''" # 'That,' she said, 'is still pointless.' str = ''''That,' she said, 'is still pointless.'''' END assert_parse_error("unexpected character found: \"'\" at line 1 column 48") do PerfectTOML.parse(<<-END) apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID END end end def test_integer exp = { "int1" => 99, "int2" => 42, "int3" => 0, "int4" => -17 } assert_equal(exp, PerfectTOML.parse(<<-'END')) int1 = +99 int2 = 42 int3 = 0 int4 = -17 END end def test_integer_with_underscore exp = { "int5" => 1000, "int6" => 5349221, "int7" => 5349221, "int8" => 12345 } assert_equal(exp, PerfectTOML.parse(<<-'END')) int5 = 1_000 int6 = 5_349_221 int7 = 53_49_221 # Indian number system grouping int8 = 1_2_3_4_5 # VALID but discouraged END end def test_integer_with_prefix exp = { "hex1" => 0xdeadbeef, "hex2" => 0xdeadbeef, "hex3" => 0xdeadbeef, "oct1" => 0o01234567, "oct2" => 0o755, "bin1" => 0b11010110, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # hexadecimal with prefix `0x` hex1 = 0xDEADBEEF hex2 = 0xdeadbeef hex3 = 0xdead_beef # octal with prefix `0o` oct1 = 0o01234567 oct2 = 0o755 # useful for Unix file permissions # binary with prefix `0b` bin1 = 0b11010110 END end def test_float exp = { "flt1" => +1.0, "flt2" => 3.1415, "flt3" => -0.01, "flt4" => 5e+22, "flt5" => 1e06, "flt6" => -2E-2, "flt7" => 6.626e-34, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # fractional flt1 = +1.0 flt2 = 3.1415 flt3 = -0.01 # exponent flt4 = 5e+22 flt5 = 1e06 flt6 = -2E-2 # both flt7 = 6.626e-34 END end def test_invalid_float assert_parse_error("unexpected character found: \".\" at line 2 column 19") do PerfectTOML.parse(<<-END) # INVALID FLOATS invalid_float_1 = .7 END end assert_parse_error("unexpected character found: \".\" at line 1 column 20") do PerfectTOML.parse(<<-END) invalid_float_2 = 7. END end assert_parse_error("unexpected character found: \".\" at line 1 column 20") do PerfectTOML.parse(<<-END) invalid_float_3 = 3.e+20 END end end def test_float_with_underscore exp = { "flt8" => 224617.445991228 } assert_equal(exp, PerfectTOML.parse(<<-'END')) flt8 = 224_617.445_991_228 END end def test_inf_and_nan toml = PerfectTOML.parse(<<-'END') # infinity sf1 = inf # positive infinity sf2 = +inf # positive infinity sf3 = -inf # negative infinity # not a number sf4 = nan # actual sNaN/qNaN encoding is implementation-specific sf5 = +nan # same as `nan` sf6 = -nan # valid, actual encoding is implementation-specific END assert(toml["sf1"].infinite?) assert(toml["sf1"] > 0) assert(toml["sf2"].infinite?) assert(toml["sf2"] > 0) assert(toml["sf3"].infinite?) assert(toml["sf3"] < 0) assert(toml["sf4"].nan?) assert(toml["sf5"].nan?) assert(toml["sf6"].nan?) end def test_boolean exp = { "bool1" => true, "bool2" => false } assert_equal(exp, PerfectTOML.parse(<<-'END')) bool1 = true bool2 = false END end def test_offset_datetime exp = { "odt1" => Time.new(1979, 5, 27, 7, 32, 0, "UTC"), "odt2" => Time.new(1979, 5, 27, 0, 32, 0, "-07:00"), "odt3" => Time.new(1979, 5, 27, 0, 32, 0.999999r, "-07:00"), "odt4" => Time.new(1979, 5, 27, 0, 32, 0, "-07:00"), } assert_equal(exp, PerfectTOML.parse(<<-'END')) odt1 = 1979-05-27T07:32:00Z odt2 = 1979-05-27T00:32:00-07:00 odt3 = 1979-05-27T00:32:00.999999-07:00 odt4 = 1979-05-27 07:32:00Z END end def test_local_datetime exp = { "ldt1" => PerfectTOML::LocalDateTime.new(1979, 5, 27, 7, 32, 0), "ldt2" => PerfectTOML::LocalDateTime.new(1979, 5, 27, 0, 32, 0.999999r), } assert_equal(exp, PerfectTOML.parse(<<-'END')) ldt1 = 1979-05-27T07:32:00 ldt2 = 1979-05-27T00:32:00.999999 END end def test_local_date exp = { "ld1" => PerfectTOML::LocalDate.new(1979, 5, 27) } assert_equal(exp, PerfectTOML.parse(<<-'END')) ld1 = 1979-05-27 END end def test_local_time exp = { "lt1" => PerfectTOML::LocalTime.new(7, 32, 0), "lt2" => PerfectTOML::LocalTime.new(0, 32, 0.999999r), } assert_equal(exp, PerfectTOML.parse(<<-'END')) lt1 = 07:32:00 lt2 = 00:32:00.999999 END end def test_array exp = { "colors" => ["red", "yellow", "green"], "contributors" => [ "Foo Bar ", { "email" => "bazqux@example.com", "name" => "Baz Qux", "url" => "https://example.com/bazqux", }, ], "integers" => [1, 2, 3], "nested_arrays_of_ints" => [[1, 2], [3, 4, 5]], "nested_mixed_array" => [[1, 2], ["a", "b", "c"]], "numbers" => [0.1, 0.2, 0.5, 1, 2, 5], "string_array" => ["all", "strings", "are the same", "type"], } assert_equal(exp, PerfectTOML.parse(<<-'END')) integers = [ 1, 2, 3 ] colors = [ "red", "yellow", "green" ] nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ] nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ] string_array = [ "all", 'strings', """are the same""", '''type''' ] # Mixed-type arrays are allowed numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ] contributors = [ "Foo Bar ", { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" } ] END end def test_table exp = { "table" => {} } assert_equal(exp, PerfectTOML.parse(<<-'END')) [table] END exp = { "table-1" => { "key1" => "some string", "key2" => 123, }, "table-2" => { "key1" => "another string", "key2" => 456, }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) [table-1] key1 = "some string" key2 = 123 [table-2] key1 = "another string" key2 = 456 END end def test_table_with_dotted_key exp = { "dog" => { "tater.man" => { "type" => { "name" => "pug" } } } } assert_equal(exp, PerfectTOML.parse(<<-'END')) [dog."tater.man"] type.name = "pug" END exp = { "a" => { "b" => { "c" => {} } }, "d" => { "e" => { "f" => {} } }, "g" => { "h" => { "i" => {} } }, "j" => { "ʞ" => { "l" => {} } }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) [a.b.c] # this is best practice [ d.e.f ] # same as [d.e.f] [ g . h . i ] # same as [g.h.i] [ j . "ʞ" . 'l' ] # same as [j."ʞ".'l'] END end def test_define_super_tables exp = { "x" => { "y" => { "z" => { "w" => {} } } }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # [x] you # [x.y] don't # [x.y.z] need these [x.y.z.w] # for this to work [x] # defining a super-table afterward is ok END end def test_redefine_table_directly assert_parse_error("cannot redefine `fruit` at line 6 column 1") do PerfectTOML.parse(<<-END) # DO NOT DO THIS [fruit] apple = "red" [fruit] orange = "orange" END end end def test_redefine_table_indirectly assert_parse_error("cannot redefine `fruit.apple` at line 6 column 1") do PerfectTOML.parse(<<-END) # DO NOT DO THIS EITHER [fruit] apple = "red" [fruit.apple] texture = "smooth" END end end def test_tables_out_of_order exp = { "fruit" => { "apple" => {}, "orange" => {}, }, "animal" => {}, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # VALID BUT DISCOURAGED [fruit.apple] [animal] [fruit.orange] END end def test_top_level_table exp = { "name" => "Fido", "breed" => "pug", "owner" => { "name" => "Regina Dogman", "member_since" => PerfectTOML::LocalDate.new(1999, 8, 4), }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) # Top-level table begins. name = "Fido" breed = "pug" # Top-level table ends. [owner] name = "Regina Dogman" member_since = 1999-08-04 END end def test_define_tables_by_dotted_keys exp = { "fruit" => { "apple" => { "color" => "red", "taste" => { "sweet" => true, }, }, }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) fruit.apple.color = "red" # Defines a table named fruit # Defines a table named fruit.apple fruit.apple.taste.sweet = true # Defines a table named fruit.apple.taste # fruit and fruit.apple were already created END end def test_redefine_tables_by_header exp = { "fruit" => { "apple" => { "color" => "red", "taste" => { "sweet" => true, }, "texture" => { "smooth" => true, }, }, }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) [fruit] apple.color = "red" apple.taste.sweet = true # [fruit.apple] # INVALID # [fruit.apple.taste] # INVALID [fruit.apple.texture] # you can add sub-tables smooth = true END assert_parse_error("cannot redefine `fruit.apple` at line 5 column 1") do PerfectTOML.parse(<<-END) [fruit] apple.color = "red" apple.taste.sweet = true [fruit.apple] # INVALID # [fruit.apple.taste] # INVALID END end assert_parse_error("cannot redefine `fruit.apple.taste` at line 5 column 1") do PerfectTOML.parse(<<-END) [fruit] apple.color = "red" apple.taste.sweet = true [fruit.apple.taste] # INVALID END end end def test_inline_table exp = { "name" => { "first" => "Tom", "last" => "Preston-Werner" }, "point" => { "x" => 1, "y" => 2 }, "animal" => { "type" => { "name" => "pug" } }, } assert_equal(exp, PerfectTOML.parse(<<-'END')) name = { first = "Tom", last = "Preston-Werner" } point = { x = 1, y = 2 } animal = { type.name = "pug" } END assert_equal(exp, PerfectTOML.parse(<<-'END')) [name] first = "Tom" last = "Preston-Werner" [point] x = 1 y = 2 [animal] type.name = "pug" END end def test_redeine_inline_table assert_parse_error("cannot redefine `product.type` at line 3 column 1") do PerfectTOML.parse(<<-END) [product] type = { name = "Nail" } type.edible = false # INVALID END end end def test_redeine_table_by_inline_table assert_parse_error("cannot redefine `product.type` at line 3 column 1") do PerfectTOML.parse(<<-END) [product] type.name = "Nail" type = { edible = false } # INVALID END end end def test_array_of_tables exp = { "products" => [ { "name" => "Hammer", "sku" => 738594937 }, { }, { "name" => "Nail", "sku" => 284758393, "color" => "gray" } ] } assert_equal(exp, PerfectTOML.parse(<<-'END')) [[products]] name = "Hammer" sku = 738594937 [[products]] # empty table within the array [[products]] name = "Nail" sku = 284758393 color = "gray" END end def test_subtable_in_array_of_tables exp = { "fruits"=> [ { "name" => "apple", "physical" => { "color" => "red", "shape" => "round" }, "varieties" => [{ "name" => "red delicious" }, { "name" => "granny smith" }], }, { "name" => "banana", "varieties" => [{"name"=>"plantain"}], }, ] } assert_equal(exp, PerfectTOML.parse(<<-'END')) [[fruits]] name = "apple" [fruits.physical] # subtable color = "red" shape = "round" [[fruits.varieties]] # nested array of tables name = "red delicious" [[fruits.varieties]] name = "granny smith" [[fruits]] name = "banana" [[fruits.varieties]] name = "plantain" END end def test_array_of_tables_after_defined assert_parse_error("cannot redefine `fruit` at line 6 column 1") do PerfectTOML.parse(<<-END) # INVALID TOML DOC [fruit.physical] # subtable, but to which parent element should it belong? color = "red" shape = "round" [[fruit]] # parser must throw an error upon discovering that "fruit" is # an array rather than a table name = "apple" END end end def test_redefine_inline_array_by_header assert_parse_error("cannot redefine `fruits` at line 4 column 1") do PerfectTOML.parse(<<-END) # INVALID TOML DOC fruits = [] [[fruits]] # Not allowed END end end def test_redefine_type_by_header assert_parse_error("cannot redefine `fruits.varieties` at line 9 column 1") do PerfectTOML.parse(<<-END) # INVALID TOML DOC [[fruits]] name = "apple" [[fruits.varieties]] name = "red delicious" # INVALID: This table conflicts with the previous array of tables [fruits.varieties] name = "granny smith" END end assert_parse_error("cannot redefine `fruits.physical` at line 10 column 1") do PerfectTOML.parse(<<-END) # INVALID TOML DOC [[fruits]] name = "apple" [fruits.physical] color = "red" shape = "round" # INVALID: This array of tables conflicts with the previous table [[fruits.physical]] color = "green" END end end def test_inline_table2 exp = { "points" => [ { "x" => 1, "y" => 2, "z" => 3 }, { "x" => 7, "y" => 8, "z" => 9 }, { "x" => 2, "y" => 4, "z" => 8 }, ] } assert_equal(exp, PerfectTOML.parse(<<-'END')) points = [ { x = 1, y = 2, z = 3 }, { x = 7, y = 8, z = 9 }, { x = 2, y = 4, z = 8 } ] END end end perfect_toml-0.9.0/test/datetime_test.rb000066400000000000000000000053311426501202400203400ustar00rootroot00000000000000require "test_helper" require "pp" class DateTimeTest < Test::Unit::TestCase def test_local_date_time d1 = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4) assert_equal(Time.new(1970, 1, 1, 2, 3, 4), d1.to_time) assert_equal(Time.new(1970, 1, 1, 2, 3, 4, "UTC"), d1.to_time("UTC")) assert_equal("1970-01-01T02:03:04", d1.to_s) assert_equal("1970-01-01T02:03:04", d1.to_inline_toml) assert_equal("#", d1.inspect) assert_equal("#\n", d1.pretty_inspect) d2 = PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4.56789r) assert_equal(Time.new(1970, 1, 1, 2, 3, 4.56789r), d2.to_time) assert_equal(Time.new(1970, 1, 1, 2, 3, 4.56789r, "UTC"), d2.to_time("UTC")) assert_equal("1970-01-01T02:03:04.567890000", d2.to_s) assert_equal("1970-01-01T02:03:04.567890000", d2.to_inline_toml) assert_equal("#", d2.inspect) assert_equal("#\n", d2.pretty_inspect) assert_equal(0, d1 <=> d1) assert_equal(-1, d1 <=> d2) end def test_local_date d1 = PerfectTOML::LocalDate.new(1970, 1, 1) assert_equal(Time.new(1970, 1, 1, 0, 0, 0), d1.to_time) assert_equal(Time.new(1970, 1, 1, 0, 0, 0, "UTC"), d1.to_time("UTC")) assert_equal("1970-01-01", d1.to_s) assert_equal("1970-01-01", d1.to_inline_toml) assert_equal("#", d1.inspect) assert_equal("#\n", d1.pretty_inspect) d2 = PerfectTOML::LocalDate.new(1970, 1, 2) assert_equal(0, d1 <=> d1) assert_equal(-1, d1 <=> d2) end def test_local_time t1 = PerfectTOML::LocalTime.new(2, 3, 4) assert_equal(Time.new(1970, 1, 1, 2, 3, 4), t1.to_time(1970, 1, 1)) assert_equal(Time.new(1970, 1, 1, 2, 3, 4, "UTC"), t1.to_time(1970, 1, 1, "UTC")) assert_equal("02:03:04", t1.to_s) assert_equal("02:03:04", t1.to_inline_toml) assert_equal("#", t1.inspect) assert_equal("#\n", t1.pretty_inspect) t2 = PerfectTOML::LocalTime.new(2, 3, 4.56789r) assert_equal(Time.new(1970, 1, 1, 2, 3, 4.56789r), t2.to_time(1970, 1, 1)) assert_equal(Time.new(1970, 1, 1, 2, 3, 4.56789r, "UTC"), t2.to_time(1970, 1, 1, "UTC")) assert_equal("02:03:04.567890000", t2.to_s) assert_equal("02:03:04.567890000", t2.to_inline_toml) assert_equal("#", t2.inspect) assert_equal("#\n", t2.pretty_inspect) assert_equal(0, t1 <=> t1) assert_equal(-1, t1 <=> t2) end end perfect_toml-0.9.0/test/error_test.rb000066400000000000000000000065761426501202400177110ustar00rootroot00000000000000require "test_helper" class ErrorTest < Test::Unit::TestCase def test_redefine_error_position assert_parse_error("cannot redefine `a` at line 2 column 3") do PerfectTOML.parse(<<-'END') a = 1 a = 2 END end assert_parse_error("cannot redefine `a` at line 2 column 3") do PerfectTOML.parse(<<-'END') a = 1 [a] END end assert_parse_error("cannot redefine `a.b` at line 3 column 3") do PerfectTOML.parse(<<-'END') [a.b] [a] b = 1 END end assert_parse_error("cannot redefine `a.b` at line 3 column 3") do PerfectTOML.parse(<<-'END') [a] b = 1 [ a . b ] END end assert_parse_error("cannot redefine `a.b` at line 3 column 3") do PerfectTOML.parse(<<-'END') [a] b = 1 [[ a . b ]] END end assert_parse_error("cannot redefine `b` at line 1 column 14") do PerfectTOML.parse(<<-'END') a = { b = 1, b = 2 } END end end def test_wrong_character assert_parse_error("unexpected character found: \"\\x00\" at line 1 column 24") do PerfectTOML.parse(<<-END) # control character -> \0 END end assert_parse_error("invalid escape character in string: \"e\" at line 1 column 7") do PerfectTOML.parse(<<-'END') a = "\e" END end assert_parse_error("invalid character in string: \"\\x00\" at line 1 column 31") do PerfectTOML.parse(<<-END) a = "raw control character -> \0" END end assert_parse_error("invalid character in string: \"\\x00\" at line 1 column 33") do PerfectTOML.parse(<<-END) a = """raw control character -> \0""" END end assert_parse_error("invalid character in string: \"\\x00\" at line 1 column 31") do PerfectTOML.parse(<<-END) a = 'raw control character -> \0' END end assert_parse_error("invalid character in string: \"\\x00\" at line 1 column 33") do PerfectTOML.parse(<<-END) a = '''raw control character -> \0''' END end end def test_unterminated_string assert_parse_error("unterminated string at line 1 column 18") do PerfectTOML.parse(<<-END.chomp) a = "unterminated END end assert_parse_error("unterminated string at line 1 column 20") do PerfectTOML.parse(<<-END.chomp) a = """unterminated END end assert_parse_error("unterminated string at line 1 column 18") do PerfectTOML.parse(<<-END.chomp) a = 'unterminated END end assert_parse_error("unterminated string at line 1 column 20") do PerfectTOML.parse(<<-END.chomp) a = '''unterminated END end end def test_wrong_offset_datetime assert_parse_error("failed to parse date or datetime \"2000-00-00T25:00:00\" at line 1 column 9") do PerfectTOML.parse(<<-'END') wrong = 2000-00-00T25:00:00Z END end end def test_wrong_local_datetime assert_parse_error("failed to parse date or datetime \"2000-00-00T25:00:00\" at line 1 column 9") do PerfectTOML.parse(<<-'END') wrong = 2000-00-00T25:00:00 END end end def test_wrong_local_date assert_parse_error("failed to parse date or datetime \"2000-00-00\" at line 1 column 9") do PerfectTOML.parse(<<-'END') wrong = 2000-00-00 END end end def test_wrong_local_time assert_parse_error("failed to parse time \"25:00:00\" at line 1 column 9") do PerfectTOML.parse(<<-'END') wrong = 25:00:00 END end end end perfect_toml-0.9.0/test/file_test.rb000066400000000000000000000007551426501202400174700ustar00rootroot00000000000000require "test_helper" require "tempfile" class LoadSaveFileTest < Test::Unit::TestCase def test_load_file Tempfile.create(["toml-test", ".toml"]) do |t| t << "load = 42\n" t.close assert_equal({ "load" => 42 }, PerfectTOML.load_file(t.path)) end end def test_save_file Tempfile.create(["toml-test", ".toml"]) do |t| t.close PerfectTOML.save_file(t.path, { "save" => 42 }) assert_equal("save = 42\n", File.read(t.path)) end end end perfect_toml-0.9.0/test/generator_test.rb000066400000000000000000000134371426501202400205400ustar00rootroot00000000000000require "test_helper" class GeneratorTest < Test::Unit::TestCase def test_generate_table exp = <<-'END' foo = 1 END assert_equal(exp, PerfectTOML.generate({ "foo" => 1 })) assert_equal(exp, PerfectTOML.generate({ :foo => 1 })) assert_equal(<<-'END', PerfectTOML.generate({ "foo" => { "bar" => 1 } })) [foo] bar = 1 END end def test_generate_array assert_equal(<<-'END', PerfectTOML.generate({ foo: [{ a: 1 }, { b: 2 }, { c: 3 }] })) [[foo]] a = 1 [[foo]] b = 2 [[foo]] c = 3 END assert_equal(<<-'END', PerfectTOML.generate({ foo: { bar: [{}, {}, {}] } })) [[foo.bar]] [[foo.bar]] [[foo.bar]] END end def test_quoted_key assert_equal(<<-'END', PerfectTOML.generate({ a: { あ: { b: { い: 1 } } } })) [a."あ".b] "い" = 1 END end def test_boolean assert_equal(<<-'END', PerfectTOML.generate({ a: true })) a = true END assert_equal(<<-'END', PerfectTOML.generate({ a: false })) a = false END end def test_float data = { a: 1.0, b: Float::INFINITY, c: -Float::INFINITY, d: Float::NAN } assert_equal(<<-'END', PerfectTOML.generate(data)) a = 1.0 b = inf c = -inf d = nan END end def test_time data = { a: Time.new(1970, 1, 1, 2, 3, 4, "UTC"), b: Time.new(1970, 1, 1, 2, 3, 4, "+00:00"), c: Time.new(1970, 1, 1, 2, 3, 4.56789, "UTC"), } assert_equal(<<-'END', PerfectTOML.generate(data)) a = 1970-01-01T02:03:04Z b = 1970-01-01T02:03:04+00:00 c = 1970-01-01T02:03:04.567890000Z END end def test_local_datetime data = { a: PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4), b: PerfectTOML::LocalDateTime.new(1970, 1, 1, 2, 3, 4.56789), } assert_equal(<<-'END', PerfectTOML.generate(data)) a = 1970-01-01T02:03:04 b = 1970-01-01T02:03:04.567890000 END end def test_local_date data = { a: PerfectTOML::LocalDate.new(1970, 1, 1), } assert_equal(<<-'END', PerfectTOML.generate(data)) a = 1970-01-01 END end def test_local_time data = { a: PerfectTOML::LocalTime.new(2, 3, 4), b: PerfectTOML::LocalTime.new(2, 3, 4.56789), } assert_equal(<<-'END', PerfectTOML.generate(data)) a = 02:03:04 b = 02:03:04.567890000 END end def test_string assert_equal(<<-'END', PerfectTOML.generate({ a: "foo" })) a = "foo" END assert_equal(<<-'END'.sub("[TAB]", "\t"), PerfectTOML.generate({ a: "foo\bbar\tbaz\nqux\fquux\rend" })) a = "foo\bbar[TAB]baz\nqux\fquux\rend" END assert_equal(<<-'END'.sub("[TAB]", "\t"), PerfectTOML.generate({ a: "foo\x00bar\x7fbaz" })) a = "foo\u0000bar\u007fbaz" END end def test_inline_array assert_equal(<<-'END', PerfectTOML.generate({ a: [[1, 2, 3], [4, 5, 6]] })) a = [[1, 2, 3], [4, 5, 6]] END assert_equal(<<-'END', PerfectTOML.generate({ a: [{}, 1] })) a = [{}, 1] END end def test_inline_table assert_equal(<<-'END', PerfectTOML.generate({ a: [{ b: { c: 42 } }, 1] })) a = [{ b = { c = 42 } }, 1] END end def test_sort_keys assert_equal(<<-'END', PerfectTOML.generate({ z: 1, a: 2 }, sort_keys: false)) z = 1 a = 2 END assert_equal(<<-'END', PerfectTOML.generate({ z: 1, a: 2 }, sort_keys: true)) a = 2 z = 1 END end def test_use_literal_string assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\"bar", b: "baz'qux" }, use_literal_string: false)) a = "foo\"bar" b = "baz'qux" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\"bar", b: "baz'qux" }, use_literal_string: true)) a = 'foo"bar' b = "baz'qux" END end def test_use_multiline_string assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"baz" }, use_multiline_string: true)) a = """ foo bar"baz""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"\"baz" }, use_multiline_string: true)) a = """ foo bar""baz""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"\"\"baz" }, use_multiline_string: true)) a = """ foo bar\"""baz""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"" }, use_multiline_string: true)) a = """ foo bar"""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"\"" }, use_multiline_string: true)) a = """ foo bar""""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar\"\"\"" }, use_multiline_string: true)) a = """ foo bar\"""""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "\n" }, use_multiline_string: true)) a = """ """ END assert_equal(<<-'END', PerfectTOML.generate({ a: "\nfoo\x00bar" }, use_multiline_string: true)) a = """ foo\u0000bar""" END end def test_use_multiline_string_with_literal_string assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar" }, use_literal_string: false, use_multiline_string: false)) a = "foo\nbar" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar" }, use_literal_string: false, use_multiline_string: true)) a = """ foo bar""" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar" }, use_literal_string: true, use_multiline_string: false)) a = "foo\nbar" END assert_equal(<<-'END', PerfectTOML.generate({ a: "foo\nbar" }, use_literal_string: true, use_multiline_string: true)) a = ''' foo bar''' END end def test_use_dot assert_equal(<<-'END', PerfectTOML.generate({ foo: { bar: 1 } }, use_dot: true)) foo.bar = 1 END assert_equal(<<-'END', PerfectTOML.generate({ foo: { bar: { baz: { qux: 1 } } } }, use_dot: true)) foo.bar.baz.qux = 1 END data = { "foo" => { "bar" => { "baz" => 1 }, "qux" => 2, } } assert_equal(<<-'END', PerfectTOML.generate(data, use_dot: true)) [foo] bar.baz = 1 qux = 2 END end def test_dup_check assert_raise(ArgumentError) do PerfectTOML.generate({ "foo" => 1, :foo => 2 }) end end end perfect_toml-0.9.0/test/parser_test.rb000066400000000000000000000032101426501202400200320ustar00rootroot00000000000000require "test_helper" # The parse is mainly tested in conformance_test.rb class MiscTest < Test::Unit::TestCase def test_symbolize_names exp = { a: 1, "あ": 2, b: { c: 3 } } assert_equal(exp, PerfectTOML.parse(<<-END, symbolize_names: true)) a = 1 "あ" = 2 b.c = 3 END end def test_mutiline_string_delimiter exp = { "a" => "str\"ing" } assert_equal(exp, PerfectTOML.parse(<<-'END')) a = """str"ing""" END exp = { "a" => "str\"\"ing" } assert_equal(exp, PerfectTOML.parse(<<-'END')) a = """str""ing""" END exp = { "a" => "string\"" } assert_equal(exp, PerfectTOML.parse(<<-'END')) a = """string"""" END exp = { "a" => "string\"\"" } assert_equal(exp, PerfectTOML.parse(<<-'END')) a = """string""""" END exp = { "a" => "string\"\"" } assert_parse_error("unexpected character found: \"\\\"\" at line 1 column 19") do PerfectTOML.parse(<<-'END') a = """string"""""" END end end def test_update_declared_table_by_value_definition assert_parse_error("cannot redefine `a.b` at line 4 column 3") do PerfectTOML.parse(<<-'END') [a.b] z = 9 [a] b.t = 9 END end end def test_unterminated_array assert_parse_error("unexpected end at line 1 column 11") do PerfectTOML.parse(<<-'END'.chomp) a = [ 1, 2 END end end def test_empty_inline_table exp = { "a" => {} } assert_equal(exp, PerfectTOML.parse(<<-'END')) a = { } END end def test_unterminated_inline_table assert_parse_error("unexpected end at line 1 column 12") do PerfectTOML.parse(<<-'END'.chomp) a = { b = 1 END end end end perfect_toml-0.9.0/test/test_helper.rb000066400000000000000000000004161426501202400200220ustar00rootroot00000000000000require "simplecov" SimpleCov.start $LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "perfect_toml" require "test-unit" module Test::Unit::Assertions def assert_parse_error(msg, &blk) assert_raise(PerfectTOML::ParseError.new(msg), &blk) end end perfect_toml-0.9.0/tool/000077500000000000000000000000001426501202400151545ustar00rootroot00000000000000perfect_toml-0.9.0/tool/decoder.rb000077500000000000000000000022671426501202400171200ustar00rootroot00000000000000#!/usr/bin/env ruby require_relative "../lib/perfect_toml" require "json" def convert(toml) case toml when Hash then toml.to_h {|k, v| [k, convert(v)] } when Array then toml.map {|v| convert(v) } when String then { "type" => "string", "value" => toml } when Integer then { "type" => "integer", "value" => toml.to_s } when Float str = toml.nan? ? "nan" : toml.infinite? ? "#{ toml > 0 ? "+" : "-" }inf" : toml.to_s str = str.sub(/\.0\z/, "") { "type" => "float", "value" => str } when true then { "type" => "bool", "value" => "true" } when false then { "type" => "bool", "value" => "false" } when Time str = toml.strftime("%Y-%m-%dT%H:%M:%S.%4N") str = str.sub(/\.0000\z/, "") zone = toml.strftime("%:z") str << (zone == "+00:00" ? "Z" : zone) { "type" => "datetime", "value" => str } when PerfectTOML::LocalDateTime { "type" => "datetime-local", "value" => toml.to_s } when PerfectTOML::LocalDate { "type" => "date-local", "value" => toml.to_s } when PerfectTOML::LocalTime { "type" => "time-local", "value" => toml.to_s } else raise "unknown type: %p" % toml end end puts JSON.generate(convert(PerfectTOML.parse($stdin.read))) perfect_toml-0.9.0/tool/encoder.rb000077500000000000000000000026251426501202400171300ustar00rootroot00000000000000#!/usr/bin/env ruby require_relative "../lib/perfect_toml" require "json" require "time" def convert(json) return json.map {|v| convert(v) } if Array === json if json.key?("type") type, val = json["type"], json["value"] case type when "integer" then val.to_i when "float" case val when "inf", "+inf" then Float::INFINITY when "-inf" then -Float::INFINITY when "nan" then -Float::NAN else val.to_f end when "string" then val when "bool" then val == "true" when "datetime" then Time.iso8601(val) when "datetime-local" raise if val !~ /\A(-?\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)\z/ PerfectTOML::LocalDateTime.new($1, $2, $3, $4, $5, $6) when "date-local" raise if val !~ /\A(-?\d{4})-(\d{2})-(\d{2})\z/ PerfectTOML::LocalDate.new($1, $2, $3) when "time-local" raise if val !~ /\A(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)\z/ PerfectTOML::LocalTime.new($1, $2, $3) else raise "unknown type: %p" % type end else json.to_h {|k, v| [k, convert(v)] } end end opts = { use_dot: ENV["TOML_ENCODER_USE_DOT"] == "1", sort_keys: ENV["TOML_ENCODER_SORT_KEYS"] == "1", use_literal_string: ENV["TOML_ENCODER_USE_LITERAL_STRING"] == "1", use_multiline_string: ENV["TOML_ENCODER_USE_MULTILINE_STRING"] == "1", } puts PerfectTOML.generate(convert(JSON.parse($stdin.read)), **opts)