├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmarks └── bench.rb ├── bin ├── console └── setup ├── lib ├── tomlrb.rb └── tomlrb │ ├── generated_parser.rb │ ├── handler.rb │ ├── local_date.rb │ ├── local_date_time.rb │ ├── local_time.rb │ ├── parser.rb │ ├── parser.y │ ├── scanner.rb │ ├── string_utils.rb │ └── version.rb ├── test ├── Cargo.toml ├── error.toml ├── example-v0.4.0.toml ├── example.toml ├── hard_example.toml ├── inf_in_keys.toml ├── minitest_helper.rb ├── nested_array_of_tables.toml ├── test_compliance.rb ├── test_key.rb ├── test_local_date_time.rb └── test_tomlrb.rb └── tomlrb.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest, macos-latest] 11 | ruby: [2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, head, jruby, jruby-head, truffleruby] 12 | exclude: 13 | - os: macos-latest 14 | ruby: 2.5 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | JRUBY_OPTS: -J-Xss16m 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: true 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | rubygems: 3.2.3 27 | - run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "toml-spec-tests"] 2 | path = toml-spec-tests 3 | url = https://github.com/KitaitiMakoto/toml-spec-tests.git 4 | [submodule "toml-test"] 5 | path = toml-test 6 | url = https://github.com/BurntSushi/toml-test.git 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.0.3 - 2022-05-28 2 | 3 | * Enable symbolize_keys: true for inline tables 4 | 5 | ### 2.0.2 - 2022-05-25 6 | 7 | * Conform to TOML v1.0.0 8 | 9 | ### 2.0.1 - 2020-12-20 10 | 11 | * Fix error with table nested under array-of-tables 12 | * Some performance improvements 13 | 14 | ### 2.0.0 - 2020-11-26 15 | 16 | * Conform to TOML v1.0.0-rc3 17 | 18 | #### Breanking Changes #### 19 | 20 | TOML v0.5.0 introduced new value types: Local Date-Time, Local Date and Local Time which represent time without time zone information. Tomlrb also introduced corresponding classes starting from v2.0.0. By this change, some table values such as `2020-11-24T20:32:18`, `2020-11-24` or `20:32:18` are not treated as strings but as new the classes `Tomlrb::LocalDateTime`, `Tomlrb::LocalDate` and `Tomlrb::LocalTime` respectively. You can get the string values by calling `#to_s` method on any of those classes. Additionally, You can also get `Time` objects with the `#to_time` method. See [API documentation](https://www.rubydoc.info/gems/tomlrb) for the methods' details. 21 | 22 | ### 1.3.0 - 2020-03-19 23 | 24 | * Fix error with falsy table values 25 | 26 | ### 1.2.9 - 2019-11-22 27 | 28 | * Fixes and cleanups for ruby 2.7 29 | 30 | ### 1.2.8 - 2018-12-18 31 | 32 | * Reduce gem size by excluding tests (tas50) 33 | * Make integer and float parsing closer to the spec (sgarciac) 34 | 35 | ### 1.2.7 - 2018-07-12 36 | 37 | * Datetime should be UTC when no offset or timezone are specified 38 | 39 | ### 1.2.6 - 2017-10-23 40 | 41 | * Fix issue where an unclosed table could make the parsing loop infinitely. 42 | * Proper string escaping 43 | 44 | ### 1.1.3 - 2015-11-24 45 | 46 | * Bare integers can be used as keys as per the spec 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem "bundler" 5 | gem "rake" 6 | gem "racc" 7 | gem "minitest" 8 | gem "minitest-reporters" 9 | gem "benchmark-ips" 10 | end 11 | 12 | gemspec 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Francois Bernier 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tomlrb 2 | 3 | [![Code Climate](https://codeclimate.com/github/fbernier/tomlrb/badges/gpa.svg)](https://codeclimate.com/github/fbernier/tomlrb) 4 | [![Gem Version](https://badge.fury.io/rb/tomlrb.svg)](http://badge.fury.io/rb/tomlrb) 5 | [![Build status](https://github.com/fbernier/tomlrb/workflows/ci/badge.svg)](https://github.com/fbernier/tomlrb/actions) 6 | 7 | A Racc based [TOML](https://github.com/toml-lang/toml) Ruby parser supporting the 1.0.0 version of the spec. 8 | 9 | ## TODO 10 | 11 | * Dumper 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'tomlrb' 19 | ``` 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install tomlrb 28 | 29 | ## Usage 30 | 31 | ```ruby 32 | Tomlrb.parse("[toml]\na = [\"array\", 123]") 33 | ``` 34 | 35 | or 36 | 37 | ```ruby 38 | Tomlrb.load_file('my_file', symbolize_keys: true) 39 | ``` 40 | 41 | ## Benchmark 42 | 43 | You can run the benchmark against the only other v0.5.0 compliant parser to my knowledge with `ruby benchmarks/bench.rb`. 44 | 45 | Here are the results on my machine: 46 | 47 | ``` 48 | Warming up -------------------------------------- 49 | emancu/toml-rb 1.000 i/100ms 50 | fbernier/tomlrb 33.000 i/100ms 51 | Calculating ------------------------------------- 52 | emancu/toml-rb 15.597 (± 6.4%) i/s - 78.000 in 5.020321s 53 | fbernier/tomlrb 348.307 (± 5.2%) i/s - 1.749k in 5.034878s 54 | 55 | Comparison: 56 | fbernier/tomlrb: 348.3 i/s 57 | emancu/toml-rb: 15.6 i/s - 22.33x (± 0.00) slower 58 | 59 | ``` 60 | 61 | ## Development 62 | 63 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. 64 | 65 | Do not forget to regenerate the parser when you modify rules in the `parser.y` file using `rake compile`. 66 | 67 | Run the tests using `rake test`. 68 | 69 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 70 | 71 | ## Contributing 72 | 73 | 1. Fork it ( https://github.com/[my-github-username]/tomlrb/fork ) 74 | 2. Create your feature branch (`git checkout -b my-new-feature`) 75 | 3. Commit your changes (`git commit -am 'Add some feature'`) 76 | 4. Push to the branch (`git push origin my-new-feature`) 77 | 5. Create a new Pull Request 78 | 79 | ## Thanks 80 | 81 | Thanks to [@jpbougie](https://github.com/jpbougie) for the crash course on the Chomsky hierarchy and general tips. 82 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # To avoid stack level too deep error on loading 2 | # toml-spec-tests/values/qa-table-inline-nested-1000.yaml 3 | ENV["RUBY_THREAD_VM_STACK_SIZE"] = "1100000" 4 | 5 | require "bundler/gem_tasks" 6 | require 'rake/testtask' 7 | 8 | Rake::TestTask.new do |t| 9 | t.libs << "test" 10 | t.test_files = FileList['test/test*.rb'] 11 | t.verbose = true 12 | end 13 | task test: :compile 14 | 15 | task default: [:test] 16 | 17 | task :compile do 18 | sh 'racc lib/tomlrb/parser.y -o lib/tomlrb/generated_parser.rb' 19 | end 20 | -------------------------------------------------------------------------------- /benchmarks/bench.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'benchmark/ips' 3 | 4 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 5 | require 'tomlrb' 6 | begin 7 | require 'toml-rb' 8 | rescue LoadError 9 | puts "Install toml-rb using 'gem install toml-rb' first." 10 | end 11 | 12 | data = File.read(File.join(__dir__, '../test/example-v0.4.0.toml')) 13 | 14 | Benchmark.ips do |x| 15 | x.report('emancu/toml-rb') do 16 | TomlRB.parse(data) 17 | end 18 | 19 | x.report('fbernier/tomlrb') do 20 | Tomlrb.parse(data) 21 | end 22 | 23 | x.compare! 24 | end 25 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'tomlrb' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | require 'byebug' 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | rake compile 7 | -------------------------------------------------------------------------------- /lib/tomlrb.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'time' 4 | require 'stringio' 5 | require 'tomlrb/version' 6 | require 'tomlrb/local_date_time' 7 | require 'tomlrb/local_date' 8 | require 'tomlrb/local_time' 9 | require 'tomlrb/string_utils' 10 | require 'tomlrb/scanner' 11 | require 'tomlrb/parser' 12 | require 'tomlrb/handler' 13 | 14 | module Tomlrb 15 | class ParseError < StandardError; end 16 | 17 | # Parses a valid TOML string into its Ruby data structure 18 | # 19 | # @param string_or_io [String, StringIO] the content 20 | # @param options [Hash] the options hash 21 | # @option options [Boolean] :symbolize_keys (false) whether to return the keys as symbols or strings 22 | # @return [Hash] the Ruby data structure represented by the input 23 | def self.parse(string_or_io, **options) 24 | io = string_or_io.is_a?(String) ? StringIO.new(string_or_io) : string_or_io 25 | scanner = Scanner.new(io) 26 | parser = Parser.new(scanner, **options) 27 | begin 28 | handler = parser.parse 29 | rescue Racc::ParseError => e 30 | raise ParseError, e.message 31 | end 32 | 33 | handler.output 34 | end 35 | 36 | # Reads a file content and parses it into its Ruby data structure 37 | # 38 | # @param path [String] the path to the file 39 | # @param options [Hash] the options hash 40 | # @option options [Boolean] :symbolize_keys (false) whether to return the keys as symbols or strings 41 | # @return [Hash] the Ruby data structure represented by the input 42 | def self.load_file(path, **options) 43 | # By default Ruby sets the external encoding of an IO object to the 44 | # default external encoding. The default external encoding is set by 45 | # locale encoding or the interpreter -E option. 46 | tmp = File.read(path, encoding: 'utf-8') 47 | Tomlrb.parse(tmp, **options) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/tomlrb/generated_parser.rb: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT MODIFY!!!! 3 | # This file is automatically generated by Racc 1.8.1 4 | # from Racc grammar file "parser.y". 5 | # 6 | 7 | require 'racc/parser.rb' 8 | module Tomlrb 9 | class GeneratedParser < Racc::Parser 10 | ##### State transition tables begin ### 11 | 12 | racc_action_table = [ 13 | 2, 45, 16, 56, 17, 44, 18, 55, 23, 19, 14 | 20, 14, 21, 22, 8, 4, 10, 36, 16, 12, 15 | 17, 48, 18, 46, 47, 19, 20, 42, 21, 22, 16 | 50, 51, 87, 86, 39, 54, 39, 72, 73, 74, 17 | 75, 70, 71, 67, 68, 65, 66, 69, 78, nil, 18 | 59, nil, nil, 12, 72, 73, 74, 75, 70, 71, 19 | 67, 68, 65, 66, 69, nil, nil, 59, nil, nil, 20 | 12, 72, 73, 74, 75, 70, 71, 67, 68, 65, 21 | 66, 69, 91, nil, 59, 89, nil, 12, 72, 73, 22 | 74, 75, 70, 71, 67, 68, 65, 66, 69, 91, 23 | nil, 59, 89, nil, 12, 72, 73, 74, 75, 70, 24 | 71, 67, 68, 65, 66, 69, 91, nil, 59, 89, 25 | nil, 12, 72, 73, 74, 75, 70, 71, 67, 68, 26 | 65, 66, 69, 91, nil, 59, 89, 29, 12, 30, 27 | nil, 31, nil, nil, 32, 33, 27, 34, 35, 29, 28 | nil, 30, 25, 31, nil, nil, 32, 33, 81, 34, 29 | 35, 16, nil, 17, 25, 18, nil, nil, 19, 20, 30 | 77, 21, 22, 16, nil, 17, nil, 18, nil, nil, 31 | 19, 20, 42, 21, 22, 16, nil, 17, nil, 18, 32 | nil, nil, 19, 20, 85, 21, 22, 95, nil, nil, 33 | 93, nil, nil, nil, 94 ] 34 | 35 | racc_action_check = [ 36 | 1, 13, 1, 41, 1, 13, 1, 41, 2, 1, 37 | 1, 1, 1, 1, 1, 1, 1, 10, 11, 1, 38 | 11, 25, 11, 24, 24, 11, 11, 11, 11, 11, 39 | 26, 26, 57, 57, 38, 40, 11, 44, 44, 44, 40 | 44, 44, 44, 44, 44, 44, 44, 44, 50, nil, 41 | 44, nil, nil, 44, 55, 55, 55, 55, 55, 55, 42 | 55, 55, 55, 55, 55, nil, nil, 55, nil, nil, 43 | 55, 58, 58, 58, 58, 58, 58, 58, 58, 58, 44 | 58, 58, 58, nil, 58, 58, nil, 58, 91, 91, 45 | 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 46 | nil, 91, 91, nil, 91, 94, 94, 94, 94, 94, 47 | 94, 94, 94, 94, 94, 94, 94, nil, 94, 94, 48 | nil, 94, 95, 95, 95, 95, 95, 95, 95, 95, 49 | 95, 95, 95, 95, nil, 95, 95, 9, 95, 9, 50 | nil, 9, nil, nil, 9, 9, 9, 9, 9, 51, 51 | nil, 51, 9, 51, nil, nil, 51, 51, 51, 51, 52 | 51, 45, nil, 45, 51, 45, nil, nil, 45, 45, 53 | 45, 45, 45, 54, nil, 54, nil, 54, nil, nil, 54 | 54, 54, 54, 54, 54, 56, nil, 56, nil, 56, 55 | nil, nil, 56, 56, 56, 56, 56, 90, nil, nil, 56 | 90, nil, nil, nil, 90 ] 57 | 58 | racc_action_pointer = [ 59 | nil, 0, 8, nil, nil, nil, nil, nil, nil, 135, 60 | 1, 16, nil, -17, nil, nil, nil, nil, nil, nil, 61 | nil, nil, nil, nil, 9, 4, 13, nil, nil, nil, 62 | nil, nil, nil, nil, nil, nil, nil, nil, 14, nil, 63 | 14, -15, nil, nil, 34, 159, nil, nil, nil, nil, 64 | 31, 147, nil, nil, 171, 51, 183, 18, 68, nil, 65 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 66 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 67 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 68 | 183, 85, nil, nil, 102, 119, nil, nil, nil ] 69 | 70 | racc_action_default = [ 71 | -1, -79, -79, -2, -3, -4, -5, -6, -7, -79, 72 | -11, -79, -31, -79, -45, -46, -47, -48, -49, -50, 73 | -51, -52, -53, 99, -79, -13, -79, -20, -21, -22, 74 | -23, -24, -25, -26, -27, -28, -10, -29, -79, -32, 75 | -33, -79, -39, -40, -67, -79, -8, -9, -12, -14, 76 | -16, -79, -30, -34, -79, -67, -79, -79, -67, -61, 77 | -62, -63, -64, -65, -66, -68, -69, -70, -71, -72, 78 | -73, -74, -75, -76, -77, -78, -43, -44, -15, -17, 79 | -18, -19, -35, -36, -37, -38, -41, -42, -54, -55, 80 | -79, -67, -56, -58, -67, -67, -57, -59, -60 ] 81 | 82 | racc_goto_table = [ 83 | 15, 24, 38, 88, 28, 37, 57, 1, 3, 5, 84 | 6, 7, 9, 49, 53, 13, 92, 83, nil, nil, 85 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 86 | nil, nil, 52, nil, nil, nil, 96, nil, nil, 97, 87 | 98, nil, nil, 79, 76, 82, 80, nil, nil, nil, 88 | nil, nil, nil, nil, nil, 84 ] 89 | 90 | racc_goto_check = [ 91 | 18, 7, 13, 22, 10, 12, 17, 1, 2, 3, 92 | 4, 5, 6, 9, 15, 19, 23, 17, nil, nil, 93 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 94 | nil, nil, 12, nil, nil, nil, 22, nil, nil, 22, 95 | 22, nil, nil, 7, 18, 13, 10, nil, nil, nil, 96 | nil, nil, nil, nil, nil, 18 ] 97 | 98 | racc_goto_pointer = [ 99 | nil, 7, 7, 8, 9, 10, 11, -8, nil, -13, 100 | -5, nil, -6, -9, nil, -26, nil, -38, -1, 14, 101 | nil, nil, -55, -74, nil, nil, nil ] 102 | 103 | racc_goto_default = [ 104 | nil, nil, nil, nil, nil, 62, nil, nil, 26, nil, 105 | nil, 11, nil, nil, 40, nil, 41, 90, 43, nil, 106 | 61, 58, nil, nil, 60, 63, 64 ] 107 | 108 | racc_reduce_table = [ 109 | 0, 0, :racc_error, 110 | 0, 24, :_reduce_none, 111 | 2, 24, :_reduce_none, 112 | 2, 24, :_reduce_none, 113 | 1, 25, :_reduce_none, 114 | 1, 25, :_reduce_none, 115 | 1, 25, :_reduce_none, 116 | 1, 25, :_reduce_none, 117 | 3, 26, :_reduce_none, 118 | 3, 26, :_reduce_none, 119 | 2, 29, :_reduce_10, 120 | 1, 29, :_reduce_11, 121 | 2, 30, :_reduce_12, 122 | 1, 30, :_reduce_13, 123 | 2, 30, :_reduce_none, 124 | 2, 32, :_reduce_15, 125 | 1, 32, :_reduce_16, 126 | 2, 32, :_reduce_none, 127 | 3, 31, :_reduce_18, 128 | 3, 31, :_reduce_19, 129 | 1, 31, :_reduce_20, 130 | 1, 31, :_reduce_21, 131 | 1, 33, :_reduce_none, 132 | 1, 33, :_reduce_23, 133 | 1, 33, :_reduce_none, 134 | 1, 33, :_reduce_none, 135 | 1, 33, :_reduce_none, 136 | 1, 33, :_reduce_none, 137 | 1, 33, :_reduce_none, 138 | 2, 28, :_reduce_none, 139 | 3, 28, :_reduce_none, 140 | 1, 34, :_reduce_31, 141 | 1, 35, :_reduce_32, 142 | 1, 36, :_reduce_none, 143 | 2, 36, :_reduce_none, 144 | 2, 38, :_reduce_none, 145 | 3, 37, :_reduce_36, 146 | 3, 39, :_reduce_37, 147 | 3, 39, :_reduce_38, 148 | 1, 39, :_reduce_39, 149 | 1, 39, :_reduce_40, 150 | 4, 27, :_reduce_41, 151 | 4, 27, :_reduce_42, 152 | 3, 42, :_reduce_43, 153 | 3, 42, :_reduce_44, 154 | 1, 42, :_reduce_45, 155 | 1, 42, :_reduce_46, 156 | 1, 41, :_reduce_none, 157 | 1, 41, :_reduce_48, 158 | 1, 41, :_reduce_none, 159 | 1, 41, :_reduce_none, 160 | 1, 41, :_reduce_none, 161 | 1, 41, :_reduce_none, 162 | 1, 41, :_reduce_none, 163 | 2, 43, :_reduce_none, 164 | 1, 45, :_reduce_55, 165 | 2, 45, :_reduce_none, 166 | 2, 45, :_reduce_none, 167 | 1, 46, :_reduce_58, 168 | 2, 46, :_reduce_none, 169 | 2, 46, :_reduce_none, 170 | 1, 44, :_reduce_61, 171 | 1, 40, :_reduce_62, 172 | 1, 40, :_reduce_none, 173 | 1, 40, :_reduce_none, 174 | 1, 47, :_reduce_none, 175 | 1, 47, :_reduce_none, 176 | 0, 49, :_reduce_none, 177 | 1, 49, :_reduce_68, 178 | 1, 49, :_reduce_69, 179 | 1, 49, :_reduce_70, 180 | 1, 49, :_reduce_71, 181 | 1, 49, :_reduce_72, 182 | 1, 49, :_reduce_73, 183 | 1, 49, :_reduce_74, 184 | 1, 48, :_reduce_75, 185 | 1, 48, :_reduce_76, 186 | 1, 48, :_reduce_77, 187 | 1, 48, :_reduce_78 ] 188 | 189 | racc_reduce_n = 79 190 | 191 | racc_shift_n = 99 192 | 193 | racc_token_table = { 194 | false => 0, 195 | :error => 1, 196 | :IDENTIFIER => 2, 197 | :STRING_MULTI => 3, 198 | :STRING_BASIC => 4, 199 | :STRING_LITERAL_MULTI => 5, 200 | :STRING_LITERAL => 6, 201 | :DATETIME => 7, 202 | :LOCAL_TIME => 8, 203 | :INTEGER => 9, 204 | :NON_DEC_INTEGER => 10, 205 | :FLOAT => 11, 206 | :FLOAT_KEYWORD => 12, 207 | :BOOLEAN => 13, 208 | :NEWLINE => 14, 209 | :EOS => 15, 210 | "[" => 16, 211 | "]" => 17, 212 | "." => 18, 213 | "{" => 19, 214 | "}" => 20, 215 | "," => 21, 216 | "=" => 22 } 217 | 218 | racc_nt_base = 23 219 | 220 | racc_use_result_var = true 221 | 222 | Racc_arg = [ 223 | racc_action_table, 224 | racc_action_check, 225 | racc_action_default, 226 | racc_action_pointer, 227 | racc_goto_table, 228 | racc_goto_check, 229 | racc_goto_default, 230 | racc_goto_pointer, 231 | racc_nt_base, 232 | racc_reduce_table, 233 | racc_token_table, 234 | racc_shift_n, 235 | racc_reduce_n, 236 | racc_use_result_var ] 237 | Ractor.make_shareable(Racc_arg) if defined?(Ractor) 238 | 239 | Racc_token_to_s_table = [ 240 | "$end", 241 | "error", 242 | "IDENTIFIER", 243 | "STRING_MULTI", 244 | "STRING_BASIC", 245 | "STRING_LITERAL_MULTI", 246 | "STRING_LITERAL", 247 | "DATETIME", 248 | "LOCAL_TIME", 249 | "INTEGER", 250 | "NON_DEC_INTEGER", 251 | "FLOAT", 252 | "FLOAT_KEYWORD", 253 | "BOOLEAN", 254 | "NEWLINE", 255 | "EOS", 256 | "\"[\"", 257 | "\"]\"", 258 | "\".\"", 259 | "\"{\"", 260 | "\"}\"", 261 | "\",\"", 262 | "\"=\"", 263 | "$start", 264 | "expressions", 265 | "expression", 266 | "table", 267 | "assignment", 268 | "inline_table", 269 | "table_start", 270 | "table_continued", 271 | "table_identifier", 272 | "table_next", 273 | "table_identifier_component", 274 | "inline_table_start", 275 | "inline_table_end", 276 | "inline_continued", 277 | "inline_assignment", 278 | "inline_next", 279 | "inline_assignment_key", 280 | "value", 281 | "assignment_key_component", 282 | "assignment_key", 283 | "array", 284 | "start_array", 285 | "array_continued", 286 | "array_next", 287 | "scalar", 288 | "string", 289 | "literal" ] 290 | Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor) 291 | 292 | Racc_debug_parser = false 293 | 294 | ##### State transition tables end ##### 295 | 296 | # reduce 0 omitted 297 | 298 | # reduce 1 omitted 299 | 300 | # reduce 2 omitted 301 | 302 | # reduce 3 omitted 303 | 304 | # reduce 4 omitted 305 | 306 | # reduce 5 omitted 307 | 308 | # reduce 6 omitted 309 | 310 | # reduce 7 omitted 311 | 312 | # reduce 8 omitted 313 | 314 | # reduce 9 omitted 315 | 316 | module_eval(<<'.,.,', 'parser.y', 18) 317 | def _reduce_10(val, _values, result) 318 | @handler.start_(:array_of_tables) 319 | result 320 | end 321 | .,., 322 | 323 | module_eval(<<'.,.,', 'parser.y', 19) 324 | def _reduce_11(val, _values, result) 325 | @handler.start_(:table) 326 | result 327 | end 328 | .,., 329 | 330 | module_eval(<<'.,.,', 'parser.y', 22) 331 | def _reduce_12(val, _values, result) 332 | array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) 333 | result 334 | end 335 | .,., 336 | 337 | module_eval(<<'.,.,', 'parser.y', 23) 338 | def _reduce_13(val, _values, result) 339 | array = @handler.end_(:table); @handler.set_context(array) 340 | result 341 | end 342 | .,., 343 | 344 | # reduce 14 omitted 345 | 346 | module_eval(<<'.,.,', 'parser.y', 27) 347 | def _reduce_15(val, _values, result) 348 | array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) 349 | result 350 | end 351 | .,., 352 | 353 | module_eval(<<'.,.,', 'parser.y', 28) 354 | def _reduce_16(val, _values, result) 355 | array = @handler.end_(:table); @handler.set_context(array) 356 | result 357 | end 358 | .,., 359 | 360 | # reduce 17 omitted 361 | 362 | module_eval(<<'.,.,', 'parser.y', 32) 363 | def _reduce_18(val, _values, result) 364 | @handler.push(val[2]) 365 | result 366 | end 367 | .,., 368 | 369 | module_eval(<<'.,.,', 'parser.y', 33) 370 | def _reduce_19(val, _values, result) 371 | val[2].split('.').each { |k| @handler.push(k) } 372 | result 373 | end 374 | .,., 375 | 376 | module_eval(<<'.,.,', 'parser.y', 35) 377 | def _reduce_20(val, _values, result) 378 | keys = val[0].split('.') 379 | @handler.start_(:table) 380 | keys.each { |key| @handler.push(key) } 381 | 382 | result 383 | end 384 | .,., 385 | 386 | module_eval(<<'.,.,', 'parser.y', 39) 387 | def _reduce_21(val, _values, result) 388 | @handler.push(val[0]) 389 | result 390 | end 391 | .,., 392 | 393 | # reduce 22 omitted 394 | 395 | module_eval(<<'.,.,', 'parser.y', 43) 396 | def _reduce_23(val, _values, result) 397 | result = StringUtils.replace_escaped_chars(val[0]) 398 | result 399 | end 400 | .,., 401 | 402 | # reduce 24 omitted 403 | 404 | # reduce 25 omitted 405 | 406 | # reduce 26 omitted 407 | 408 | # reduce 27 omitted 409 | 410 | # reduce 28 omitted 411 | 412 | # reduce 29 omitted 413 | 414 | # reduce 30 omitted 415 | 416 | module_eval(<<'.,.,', 'parser.y', 55) 417 | def _reduce_31(val, _values, result) 418 | @handler.start_(:inline) 419 | result 420 | end 421 | .,., 422 | 423 | module_eval(<<'.,.,', 'parser.y', 59) 424 | def _reduce_32(val, _values, result) 425 | array = @handler.end_(:inline) 426 | @handler.push_inline(array) 427 | 428 | result 429 | end 430 | .,., 431 | 432 | # reduce 33 omitted 433 | 434 | # reduce 34 omitted 435 | 436 | # reduce 35 omitted 437 | 438 | module_eval(<<'.,.,', 'parser.y', 72) 439 | def _reduce_36(val, _values, result) 440 | keys = @handler.end_(:inline_keys) 441 | @handler.push(keys) 442 | 443 | result 444 | end 445 | .,., 446 | 447 | module_eval(<<'.,.,', 'parser.y', 78) 448 | def _reduce_37(val, _values, result) 449 | @handler.push(val[2]) 450 | 451 | result 452 | end 453 | .,., 454 | 455 | module_eval(<<'.,.,', 'parser.y', 80) 456 | def _reduce_38(val, _values, result) 457 | val[2].split('.').each { |k| @handler.push(k) } 458 | result 459 | end 460 | .,., 461 | 462 | module_eval(<<'.,.,', 'parser.y', 82) 463 | def _reduce_39(val, _values, result) 464 | keys = val[0].split('.') 465 | @handler.start_(:inline_keys) 466 | keys.each { |key| @handler.push(key) } 467 | 468 | result 469 | end 470 | .,., 471 | 472 | module_eval(<<'.,.,', 'parser.y', 87) 473 | def _reduce_40(val, _values, result) 474 | @handler.start_(:inline_keys) 475 | @handler.push(val[0]) 476 | 477 | result 478 | end 479 | .,., 480 | 481 | module_eval(<<'.,.,', 'parser.y', 93) 482 | def _reduce_41(val, _values, result) 483 | keys = @handler.end_(:keys) 484 | value = keys.pop 485 | @handler.validate_value(value) 486 | @handler.push(value) 487 | @handler.assign(keys) 488 | 489 | result 490 | end 491 | .,., 492 | 493 | module_eval(<<'.,.,', 'parser.y', 100) 494 | def _reduce_42(val, _values, result) 495 | keys = @handler.end_(:keys) 496 | value = keys.pop 497 | @handler.validate_value(value) 498 | @handler.push(value) 499 | @handler.assign(keys) 500 | 501 | result 502 | end 503 | .,., 504 | 505 | module_eval(<<'.,.,', 'parser.y', 108) 506 | def _reduce_43(val, _values, result) 507 | @handler.push(val[2]) 508 | result 509 | end 510 | .,., 511 | 512 | module_eval(<<'.,.,', 'parser.y', 109) 513 | def _reduce_44(val, _values, result) 514 | val[2].split('.').each { |k| @handler.push(k) } 515 | result 516 | end 517 | .,., 518 | 519 | module_eval(<<'.,.,', 'parser.y', 111) 520 | def _reduce_45(val, _values, result) 521 | keys = val[0].split('.') 522 | @handler.start_(:keys) 523 | keys.each { |key| @handler.push(key) } 524 | 525 | result 526 | end 527 | .,., 528 | 529 | module_eval(<<'.,.,', 'parser.y', 115) 530 | def _reduce_46(val, _values, result) 531 | @handler.start_(:keys); @handler.push(val[0]) 532 | result 533 | end 534 | .,., 535 | 536 | # reduce 47 omitted 537 | 538 | module_eval(<<'.,.,', 'parser.y', 119) 539 | def _reduce_48(val, _values, result) 540 | result = StringUtils.replace_escaped_chars(val[0]) 541 | result 542 | end 543 | .,., 544 | 545 | # reduce 49 omitted 546 | 547 | # reduce 50 omitted 548 | 549 | # reduce 51 omitted 550 | 551 | # reduce 52 omitted 552 | 553 | # reduce 53 omitted 554 | 555 | # reduce 54 omitted 556 | 557 | module_eval(<<'.,.,', 'parser.y', 130) 558 | def _reduce_55(val, _values, result) 559 | array = @handler.end_(:array); @handler.push(array.compact) 560 | result 561 | end 562 | .,., 563 | 564 | # reduce 56 omitted 565 | 566 | # reduce 57 omitted 567 | 568 | module_eval(<<'.,.,', 'parser.y', 135) 569 | def _reduce_58(val, _values, result) 570 | array = @handler.end_(:array); @handler.push(array.compact) 571 | result 572 | end 573 | .,., 574 | 575 | # reduce 59 omitted 576 | 577 | # reduce 60 omitted 578 | 579 | module_eval(<<'.,.,', 'parser.y', 140) 580 | def _reduce_61(val, _values, result) 581 | @handler.start_(:array) 582 | result 583 | end 584 | .,., 585 | 586 | module_eval(<<'.,.,', 'parser.y', 143) 587 | def _reduce_62(val, _values, result) 588 | @handler.push(val[0]) 589 | result 590 | end 591 | .,., 592 | 593 | # reduce 63 omitted 594 | 595 | # reduce 64 omitted 596 | 597 | # reduce 65 omitted 598 | 599 | # reduce 66 omitted 600 | 601 | # reduce 67 omitted 602 | 603 | module_eval(<<'.,.,', 'parser.y', 152) 604 | def _reduce_68(val, _values, result) 605 | result = val[0].to_f 606 | result 607 | end 608 | .,., 609 | 610 | module_eval(<<'.,.,', 'parser.y', 154) 611 | def _reduce_69(val, _values, result) 612 | v = val[0] 613 | result = if v.end_with?('nan') 614 | Float::NAN 615 | else 616 | (v[0] == '-' ? -1 : 1) * Float::INFINITY 617 | end 618 | 619 | result 620 | end 621 | .,., 622 | 623 | module_eval(<<'.,.,', 'parser.y', 161) 624 | def _reduce_70(val, _values, result) 625 | result = val[0].to_i 626 | result 627 | end 628 | .,., 629 | 630 | module_eval(<<'.,.,', 'parser.y', 163) 631 | def _reduce_71(val, _values, result) 632 | base = case val[0][1] 633 | when 'x' then 16 634 | when 'o' then 8 635 | when 'b' then 2 636 | end 637 | result = val[0].to_i(base) 638 | 639 | result 640 | end 641 | .,., 642 | 643 | module_eval(<<'.,.,', 'parser.y', 170) 644 | def _reduce_72(val, _values, result) 645 | result = val[0] == 'true' ? true : false 646 | result 647 | end 648 | .,., 649 | 650 | module_eval(<<'.,.,', 'parser.y', 172) 651 | def _reduce_73(val, _values, result) 652 | v = val[0] 653 | result = if v[6].nil? 654 | if v[4].nil? 655 | LocalDate.new(v[0], v[1], v[2]) 656 | else 657 | LocalDateTime.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f) 658 | end 659 | else 660 | # Patch for 24:00:00 which Ruby parses 661 | if v[3].to_i == 24 && v[4].to_i == 0 && v[5].to_i == 0 662 | v[3] = (v[3].to_i + 1).to_s 663 | end 664 | 665 | Time.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f, v[6]) 666 | end 667 | 668 | result 669 | end 670 | .,., 671 | 672 | module_eval(<<'.,.,', 'parser.y', 188) 673 | def _reduce_74(val, _values, result) 674 | result = LocalTime.new(*val[0]) 675 | result 676 | end 677 | .,., 678 | 679 | module_eval(<<'.,.,', 'parser.y', 191) 680 | def _reduce_75(val, _values, result) 681 | result = StringUtils.replace_escaped_chars(StringUtils.multiline_replacements(val[0])) 682 | result 683 | end 684 | .,., 685 | 686 | module_eval(<<'.,.,', 'parser.y', 192) 687 | def _reduce_76(val, _values, result) 688 | result = StringUtils.replace_escaped_chars(val[0]) 689 | result 690 | end 691 | .,., 692 | 693 | module_eval(<<'.,.,', 'parser.y', 193) 694 | def _reduce_77(val, _values, result) 695 | result = StringUtils.strip_spaces(val[0]) 696 | result 697 | end 698 | .,., 699 | 700 | module_eval(<<'.,.,', 'parser.y', 194) 701 | def _reduce_78(val, _values, result) 702 | result = val[0] 703 | result 704 | end 705 | .,., 706 | 707 | def _reduce_none(val, _values, result) 708 | val[0] 709 | end 710 | 711 | end # class GeneratedParser 712 | end # module Tomlrb 713 | -------------------------------------------------------------------------------- /lib/tomlrb/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Tomlrb 4 | class Handler 5 | attr_reader :output, :symbolize_keys 6 | 7 | def initialize(**options) 8 | @output = {} 9 | @current = @output 10 | @stack = [] 11 | @array_names = [] 12 | @current_table = [] 13 | @keys = Keys.new 14 | @symbolize_keys = options[:symbolize_keys] 15 | end 16 | 17 | def set_context(identifiers, is_array_of_tables: false) 18 | if identifiers.empty? 19 | raise ParseError, 'Array needs a name' 20 | end 21 | 22 | @current_table = identifiers.dup 23 | @keys.add_table_key identifiers, is_array_of_tables 24 | @current = @output 25 | 26 | deal_with_array_of_tables(identifiers, is_array_of_tables) do |identifierz| 27 | identifierz.each do |k| 28 | k = k.to_sym if @symbolize_keys 29 | if @current[k].is_a?(Array) 30 | @current = @current[k].last 31 | else 32 | @current[k] ||= {} 33 | @current = @current[k] 34 | end 35 | end 36 | end 37 | end 38 | 39 | def deal_with_array_of_tables(identifiers, is_array_of_tables) 40 | stringified_identifier = identifiers.join('.') 41 | 42 | if is_array_of_tables 43 | @array_names << stringified_identifier 44 | last_identifier = identifiers.pop 45 | elsif @array_names.include?(stringified_identifier) 46 | raise ParseError, 'Cannot define a normal table with the same name as an already established array' 47 | end 48 | 49 | yield(identifiers) 50 | 51 | if is_array_of_tables 52 | last_identifier = last_identifier.to_sym if @symbolize_keys 53 | @current[last_identifier] ||= [] 54 | raise ParseError, "Cannot use key #{last_identifier} for both table and array at once" unless @current[last_identifier].respond_to?(:<<) 55 | @current[last_identifier] << {} 56 | @current = @current[last_identifier].last 57 | end 58 | end 59 | 60 | def assign(k) 61 | @keys.add_pair_key k, @current_table 62 | current = @current 63 | while (key = k.shift) 64 | key = key.to_sym if @symbolize_keys 65 | current = assign_key_path(current, key, k.empty?) 66 | end 67 | end 68 | 69 | def push(o) 70 | @stack << o 71 | end 72 | 73 | def push_inline(inline_arrays) 74 | merged_inline = {} 75 | 76 | inline_arrays.each do |inline_array| 77 | current = merged_inline 78 | value = inline_array.pop 79 | inline_array.each_with_index do |inline_key, inline_index| 80 | inline_key = inline_key.to_sym if @symbolize_keys 81 | last_key = inline_index == inline_array.size - 1 82 | 83 | if last_key 84 | if current[inline_key].nil? 85 | current[inline_key] = value 86 | else 87 | raise Key::KeyConflict, "Inline key #{inline_key} is already used" 88 | end 89 | else 90 | current[inline_key] ||= {} 91 | current = current[inline_key] 92 | end 93 | end 94 | end 95 | 96 | push(merged_inline) 97 | end 98 | 99 | def start_(type) 100 | push([type]) 101 | end 102 | 103 | def end_(type) 104 | array = [] 105 | while (value = @stack.pop) != [type] 106 | raise ParseError, 'Unclosed table' if @stack.empty? 107 | 108 | array.unshift(value) 109 | end 110 | array 111 | end 112 | 113 | def validate_value(value) 114 | if value.nil? 115 | raise ParseError, 'Value must be present' 116 | end 117 | end 118 | 119 | private 120 | 121 | def assign_key_path(current, key, key_emptied) 122 | if key_emptied 123 | raise ParseError, "Cannot overwrite value with key #{key}" unless current.is_a?(Hash) 124 | 125 | current[key] = @stack.pop 126 | return current 127 | end 128 | current[key] ||= {} 129 | current[key] 130 | end 131 | end 132 | 133 | class Keys 134 | def initialize 135 | @keys = {} 136 | end 137 | 138 | def add_table_key(keys, is_array_of_tables = false) 139 | self << [keys, [], is_array_of_tables] 140 | end 141 | 142 | def add_pair_key(keys, context) 143 | self << [context, keys, false] 144 | end 145 | 146 | def <<(keys) 147 | table_keys, pair_keys, is_array_of_tables = keys 148 | current = @keys 149 | current = append_table_keys(current, table_keys, pair_keys.empty?, is_array_of_tables) 150 | append_pair_keys(current, pair_keys, table_keys.empty?, is_array_of_tables) 151 | end 152 | 153 | private 154 | 155 | def append_table_keys(current, table_keys, pair_keys_empty, is_array_of_tables) 156 | table_keys.each_with_index do |key, index| 157 | declared = (index == table_keys.length - 1) && pair_keys_empty 158 | if index.zero? 159 | current = find_or_create_first_table_key(current, key, declared, is_array_of_tables) 160 | else 161 | current <<= [key, :table, declared, is_array_of_tables] 162 | end 163 | end 164 | 165 | current.clear_children if is_array_of_tables 166 | current 167 | end 168 | 169 | def find_or_create_first_table_key(current, key, declared, is_array_of_tables) 170 | existed = current[key] 171 | if existed && existed.type == :pair 172 | raise Key::KeyConflict, "Key #{key} is already used as #{existed.type} key" 173 | end 174 | if existed && existed.declared? && declared && ! is_array_of_tables 175 | raise Key::KeyConflict, "Key #{key} is already used" 176 | end 177 | k = existed || Key.new(key, :table, declared) 178 | current[key] = k 179 | k 180 | end 181 | 182 | def append_pair_keys(current, pair_keys, table_keys_empty, is_array_of_tables) 183 | pair_keys.each_with_index do |key, index| 184 | declared = index == pair_keys.length - 1 185 | if index.zero? && table_keys_empty 186 | current = find_or_create_first_pair_key(current, key, declared, table_keys_empty) 187 | else 188 | key = current << [key, :pair, declared, is_array_of_tables] 189 | current = key 190 | end 191 | end 192 | end 193 | 194 | def find_or_create_first_pair_key(current, key, declared, table_keys_empty) 195 | existed = current[key] 196 | if existed && (existed.type == :pair) && declared && table_keys_empty 197 | raise Key::KeyConflict, "Key #{key} is already used" 198 | end 199 | k = Key.new(key, :pair, declared) 200 | current[key] = k 201 | k 202 | end 203 | end 204 | 205 | class Key 206 | class KeyConflict < ParseError; end 207 | 208 | attr_reader :key, :type 209 | 210 | def initialize(key, type, declared = false) 211 | @key = key 212 | @type = type 213 | @declared = declared 214 | @children = {} 215 | end 216 | 217 | def declared? 218 | @declared 219 | end 220 | 221 | def <<(key_type_declared) 222 | key, type, declared, is_array_of_tables = key_type_declared 223 | existed = @children[key] 224 | validate_already_declared_as_different_key(type, declared, existed) 225 | validate_already_declared_as_non_array_table(type, is_array_of_tables, declared, existed) 226 | validate_path_already_created_as_different_type(type, declared, existed) 227 | validate_path_already_declared_as_different_type(type, declared, existed) 228 | validate_already_declared_as_same_key(declared, existed) 229 | @children[key] = existed || self.class.new(key, type, declared) 230 | end 231 | 232 | def clear_children 233 | @children.clear 234 | end 235 | 236 | private 237 | 238 | def validate_already_declared_as_different_key(type, _declared, existed) 239 | if existed && existed.declared? && existed.type != type 240 | raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key" 241 | end 242 | end 243 | 244 | def validate_already_declared_as_non_array_table(type, is_array_of_tables, declared, existed) 245 | if declared && type == :table && existed && existed.declared? && !is_array_of_tables 246 | raise KeyConflict, "Key #{existed.key} is already used" 247 | end 248 | end 249 | 250 | def validate_path_already_created_as_different_type(type, declared, existed) 251 | if declared && (type == :table) && existed && (existed.type == :pair) && !existed.declared? 252 | raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key" 253 | end 254 | end 255 | 256 | def validate_path_already_declared_as_different_type(type, declared, existed) 257 | if !declared && (type == :pair) && existed && (existed.type == :pair) && existed.declared? 258 | raise KeyConflict, "Key #{key} is already used as #{type} key" 259 | end 260 | end 261 | 262 | def validate_already_declared_as_same_key(declared, existed) 263 | if existed && !existed.declared? && declared 264 | raise KeyConflict, "Key #{existed.key} is already used as #{existed.type} key" 265 | end 266 | end 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /lib/tomlrb/local_date.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'forwardable' 4 | 5 | module Tomlrb 6 | class LocalDate 7 | extend Forwardable 8 | 9 | def_delegators :@time, :year, :month, :day 10 | 11 | def initialize(year, month, day) 12 | @time = Time.new(year, month, day, 0, 0, 0, '-00:00') 13 | end 14 | 15 | # @param offset see {LocalDateTime#to_time} 16 | # @return [Time] 00:00:00 of the date 17 | def to_time(offset = '-00:00') 18 | return @time if offset == '-00:00' 19 | Time.new(year, month, day, 0, 0, 0, offset) 20 | end 21 | 22 | def to_s 23 | @time.strftime('%F') 24 | end 25 | 26 | def ==(other) 27 | other.is_a?(self.class) && 28 | to_time == other.to_time 29 | end 30 | 31 | def inspect 32 | "#<#{self.class}: #{self}>" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/tomlrb/local_date_time.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'forwardable' 4 | 5 | module Tomlrb 6 | class LocalDateTime 7 | extend Forwardable 8 | 9 | def_delegators :@time, :year, :month, :day, :hour, :min, :sec, :usec, :nsec 10 | 11 | def initialize(year, month, day, hour, min, sec) # rubocop:disable Metrics/ParameterLists 12 | @time = Time.new(year, month, day, hour, min, sec, '-00:00') 13 | @sec = sec 14 | end 15 | 16 | # @param offset [String, Symbol, Numeric, nil] time zone offset. 17 | # * when +String+, must be '+HH:MM' format, '-HH:MM' format, 'UTC', 'A'..'I' or 'K'..'Z'. Arguments excluding '+-HH:MM' are supporeted at Ruby >= 2.7.0 18 | # * when +Symbol+, must be +:dst+(for summar time for local) or +:std+(for standard time). 19 | # * when +Numeric+, it is time zone offset in second. 20 | # * when +nil+, local time zone offset is used. 21 | # @return [Time] 22 | def to_time(offset = '-00:00') 23 | return @time if offset == '-00:00' 24 | Time.new(year, month, day, hour, min, @sec, offset) 25 | end 26 | 27 | def to_s 28 | frac = (@sec - sec) 29 | frac_str = frac.zero? ? '' : frac.to_s[1..-1] 30 | @time.strftime('%FT%T') << frac_str 31 | end 32 | 33 | def ==(other) 34 | other.is_a?(self.class) && 35 | to_time == other.to_time 36 | end 37 | 38 | def inspect 39 | "#<#{self.class}: #{self}>" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/tomlrb/local_time.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'forwardable' 4 | 5 | module Tomlrb 6 | class LocalTime 7 | extend Forwardable 8 | 9 | def_delegators :@time, :hour, :min, :sec, :usec, :nsec 10 | 11 | def initialize(hour, min, sec) 12 | @time = Time.new(0, 1, 1, hour, min, sec, '-00:00') 13 | @sec = sec 14 | end 15 | 16 | # @param year [Integer] 17 | # @param month [Integer] 18 | # @param day [Integer] 19 | # @param offset see {LocalDateTime#to_time} 20 | # @return [Time] the time of the date specified by params 21 | def to_time(year, month, day, offset = '-00:00') 22 | Time.new(year, month, day, hour, min, @sec, offset) 23 | end 24 | 25 | def to_s 26 | frac = (@sec - sec) 27 | frac_str = frac.zero? ? '' : frac.to_s[1..-1] 28 | @time.strftime('%T') << frac_str 29 | end 30 | 31 | def ==(other) 32 | other.is_a?(self.class) && 33 | @time == other.to_time(0, 1, 1) 34 | end 35 | 36 | def inspect 37 | "#<#{self.class}: #{self}>" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tomlrb/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'tomlrb/generated_parser' 4 | 5 | class Tomlrb::Parser < Tomlrb::GeneratedParser 6 | 7 | def initialize(tokenizer, **options) 8 | @tokenizer = tokenizer 9 | @handler = Tomlrb::Handler.new(**options) 10 | super() 11 | end 12 | 13 | def next_token 14 | @tokenizer.next_token 15 | end 16 | 17 | def parse 18 | do_parse 19 | @handler 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tomlrb/parser.y: -------------------------------------------------------------------------------- 1 | class Tomlrb::GeneratedParser 2 | token IDENTIFIER STRING_MULTI STRING_BASIC STRING_LITERAL_MULTI STRING_LITERAL DATETIME LOCAL_TIME INTEGER NON_DEC_INTEGER FLOAT FLOAT_KEYWORD BOOLEAN NEWLINE EOS 3 | rule 4 | expressions 5 | | expressions expression 6 | | expressions EOS 7 | ; 8 | expression 9 | : table 10 | | assignment 11 | | inline_table 12 | | NEWLINE 13 | ; 14 | table 15 | : table_start table_continued NEWLINE 16 | | table_start table_continued EOS 17 | ; 18 | table_start 19 | : '[' '[' { @handler.start_(:array_of_tables) } 20 | | '[' { @handler.start_(:table) } 21 | ; 22 | table_continued 23 | : ']' ']' { array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) } 24 | | ']' { array = @handler.end_(:table); @handler.set_context(array) } 25 | | table_identifier table_next 26 | ; 27 | table_next 28 | : ']' ']' { array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) } 29 | | ']' { array = @handler.end_(:table); @handler.set_context(array) } 30 | | '.' table_continued 31 | ; 32 | table_identifier 33 | : table_identifier '.' table_identifier_component { @handler.push(val[2]) } 34 | | table_identifier '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } } 35 | | FLOAT { 36 | keys = val[0].split('.') 37 | @handler.start_(:table) 38 | keys.each { |key| @handler.push(key) } 39 | } 40 | | table_identifier_component { @handler.push(val[0]) } 41 | ; 42 | table_identifier_component 43 | : IDENTIFIER 44 | | STRING_BASIC { result = StringUtils.replace_escaped_chars(val[0]) } 45 | | STRING_LITERAL 46 | | INTEGER 47 | | NON_DEC_INTEGER 48 | | FLOAT_KEYWORD 49 | | BOOLEAN 50 | ; 51 | inline_table 52 | : inline_table_start inline_table_end 53 | | inline_table_start inline_continued inline_table_end 54 | ; 55 | inline_table_start 56 | : '{' { @handler.start_(:inline) } 57 | ; 58 | inline_table_end 59 | : '}' { 60 | array = @handler.end_(:inline) 61 | @handler.push_inline(array) 62 | } 63 | ; 64 | inline_continued 65 | : inline_assignment 66 | | inline_assignment inline_next 67 | ; 68 | inline_next 69 | : ',' inline_continued 70 | ; 71 | inline_assignment 72 | : inline_assignment_key '=' value { 73 | keys = @handler.end_(:inline_keys) 74 | @handler.push(keys) 75 | } 76 | ; 77 | inline_assignment_key 78 | : inline_assignment_key '.' assignment_key_component { 79 | @handler.push(val[2]) 80 | } 81 | | inline_assignment_key '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } } 82 | | FLOAT { 83 | keys = val[0].split('.') 84 | @handler.start_(:inline_keys) 85 | keys.each { |key| @handler.push(key) } 86 | } 87 | | assignment_key_component { 88 | @handler.start_(:inline_keys) 89 | @handler.push(val[0]) 90 | } 91 | ; 92 | assignment 93 | : assignment_key '=' value EOS { 94 | keys = @handler.end_(:keys) 95 | value = keys.pop 96 | @handler.validate_value(value) 97 | @handler.push(value) 98 | @handler.assign(keys) 99 | } 100 | | assignment_key '=' value NEWLINE { 101 | keys = @handler.end_(:keys) 102 | value = keys.pop 103 | @handler.validate_value(value) 104 | @handler.push(value) 105 | @handler.assign(keys) 106 | } 107 | ; 108 | assignment_key 109 | : assignment_key '.' assignment_key_component { @handler.push(val[2]) } 110 | | assignment_key '.' FLOAT { val[2].split('.').each { |k| @handler.push(k) } } 111 | | FLOAT { 112 | keys = val[0].split('.') 113 | @handler.start_(:keys) 114 | keys.each { |key| @handler.push(key) } 115 | } 116 | | assignment_key_component { @handler.start_(:keys); @handler.push(val[0]) } 117 | ; 118 | assignment_key_component 119 | : IDENTIFIER 120 | | STRING_BASIC { result = StringUtils.replace_escaped_chars(val[0]) } 121 | | STRING_LITERAL 122 | | INTEGER 123 | | NON_DEC_INTEGER 124 | | FLOAT_KEYWORD 125 | | BOOLEAN 126 | ; 127 | array 128 | : start_array array_continued 129 | ; 130 | array_continued 131 | : ']' { array = @handler.end_(:array); @handler.push(array.compact) } 132 | | value array_next 133 | | NEWLINE array_continued 134 | ; 135 | array_next 136 | : ']' { array = @handler.end_(:array); @handler.push(array.compact) } 137 | | ',' array_continued 138 | | NEWLINE array_continued 139 | ; 140 | start_array 141 | : '[' { @handler.start_(:array) } 142 | ; 143 | value 144 | : scalar { @handler.push(val[0]) } 145 | | array 146 | | inline_table 147 | ; 148 | scalar 149 | : string 150 | | literal 151 | ; 152 | literal 153 | | FLOAT { result = val[0].to_f } 154 | | FLOAT_KEYWORD { 155 | v = val[0] 156 | result = if v.end_with?('nan') 157 | Float::NAN 158 | else 159 | (v[0] == '-' ? -1 : 1) * Float::INFINITY 160 | end 161 | } 162 | | INTEGER { result = val[0].to_i } 163 | | NON_DEC_INTEGER { 164 | base = case val[0][1] 165 | when 'x' then 16 166 | when 'o' then 8 167 | when 'b' then 2 168 | end 169 | result = val[0].to_i(base) 170 | } 171 | | BOOLEAN { result = val[0] == 'true' ? true : false } 172 | | DATETIME { 173 | v = val[0] 174 | result = if v[6].nil? 175 | if v[4].nil? 176 | LocalDate.new(v[0], v[1], v[2]) 177 | else 178 | LocalDateTime.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f) 179 | end 180 | else 181 | # Patch for 24:00:00 which Ruby parses 182 | if v[3].to_i == 24 && v[4].to_i == 0 && v[5].to_i == 0 183 | v[3] = (v[3].to_i + 1).to_s 184 | end 185 | 186 | Time.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f, v[6]) 187 | end 188 | } 189 | | LOCAL_TIME { result = LocalTime.new(*val[0]) } 190 | ; 191 | string 192 | : STRING_MULTI { result = StringUtils.replace_escaped_chars(StringUtils.multiline_replacements(val[0])) } 193 | | STRING_BASIC { result = StringUtils.replace_escaped_chars(val[0]) } 194 | | STRING_LITERAL_MULTI { result = StringUtils.strip_spaces(val[0]) } 195 | | STRING_LITERAL { result = val[0] } 196 | ; 197 | -------------------------------------------------------------------------------- /lib/tomlrb/scanner.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'strscan' 4 | 5 | module Tomlrb 6 | class Scanner 7 | COMMENT = 8 | /#[^\u0000-\u0008\u000A-\u001F\u007F]*/.freeze 9 | IDENTIFIER = 10 | /[A-Za-z0-9_-]+/.freeze 11 | SPACE = 12 | /[ \t]/.freeze 13 | NEWLINE = 14 | /(?:[ \t]*(?:\r?\n)[ \t]*)+/.freeze 15 | STRING_BASIC = 16 | /(")(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F])*?\1/.freeze 17 | STRING_MULTI = 18 | /"{3}([^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*?(? "\t", 7 | '\\b' => "\b", 8 | '\\f' => "\f", 9 | '\\n' => "\n", 10 | '\\r' => "\r", 11 | '\\"' => '"', 12 | '\\\\' => '\\' 13 | }.freeze 14 | 15 | def self.multiline_replacements(str) 16 | strip_spaces(str).gsub(/\\+\s*\n\s*/) do |matched| 17 | if matched.match(/\\+/)[0].length.odd? 18 | matched.gsub(/\\\s*\n\s*/, '') 19 | else 20 | matched 21 | end 22 | end 23 | end 24 | 25 | def self.replace_escaped_chars(str) 26 | str.gsub(/\\(u[\da-fA-F]{4}|U[\da-fA-F]{8}|.)/) do |m| 27 | if m.size == 2 28 | SPECIAL_CHARS[m] || (raise Tomlrb::ParseError.new "Escape sequence #{m} is reserved") 29 | else 30 | m[2..-1].to_i(16).chr(Encoding::UTF_8) 31 | end 32 | end 33 | end 34 | 35 | def self.strip_spaces(str) 36 | str[0] = '' if str[0] == "\n" 37 | str 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tomlrb/version.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Tomlrb 4 | VERSION = '2.0.3' 5 | end 6 | -------------------------------------------------------------------------------- /test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uutils" 3 | version = "0.0.1" 4 | authors = [] 5 | build = "build.rs" 6 | 7 | [features] 8 | unix = [ 9 | "arch", 10 | "chmod", 11 | "chown", 12 | "chroot", 13 | "du", 14 | "groups", 15 | "hostid", 16 | "hostname", 17 | "id", 18 | "install", 19 | "kill", 20 | "logname", 21 | "mkfifo", 22 | "mknod", 23 | "mv", 24 | "nice", 25 | "nohup", 26 | "pathchk", 27 | "pinky", 28 | "stat", 29 | "stdbuf", 30 | "timeout", 31 | "touch", 32 | "tty", 33 | "uname", 34 | "unlink", 35 | "uptime", 36 | "users", 37 | ] 38 | generic = [ 39 | "base64", 40 | "basename", 41 | "cat", 42 | "cksum", 43 | "comm", 44 | "cp", 45 | "cut", 46 | "dircolors", 47 | "dirname", 48 | "echo", 49 | "env", 50 | "expand", 51 | "expr", 52 | "factor", 53 | "false", 54 | "fmt", 55 | "fold", 56 | "hashsum", 57 | "head", 58 | "link", 59 | "ln", 60 | "ls", 61 | "mkdir", 62 | "mktemp", 63 | "nl", 64 | "nproc", 65 | "od", 66 | "paste", 67 | "printenv", 68 | "printf", 69 | "ptx", 70 | "pwd", 71 | "readlink", 72 | "realpath", 73 | "relpath", 74 | "rm", 75 | "rmdir", 76 | "seq", 77 | "shred", 78 | "shuf", 79 | "sleep", 80 | "sort", 81 | "split", 82 | "sum", 83 | "sync", 84 | "tac", 85 | "tail", 86 | "tee", 87 | "test", 88 | "tr", 89 | "true", 90 | "truncate", 91 | "tsort", 92 | "unexpand", 93 | "uniq", 94 | "wc", 95 | "whoami", 96 | "yes", 97 | ] 98 | test_unimplemented = [] 99 | nightly = [] 100 | default = ["generic", "unix"] 101 | 102 | [dependencies] 103 | uucore = { path="src/uucore" } 104 | arch = { optional=true, path="src/arch" } 105 | base64 = { optional=true, path="src/base64" } 106 | basename = { optional=true, path="src/basename" } 107 | cat = { optional=true, path="src/cat" } 108 | chmod = { optional=true, path="src/chmod" } 109 | chown = { optional=true, path="src/chown" } 110 | chroot = { optional=true, path="src/chroot" } 111 | cksum = { optional=true, path="src/cksum" } 112 | comm = { optional=true, path="src/comm" } 113 | cp = { optional=true, path="src/cp" } 114 | cut = { optional=true, path="src/cut" } 115 | dircolors= { optional=true, path="src/dircolors" } 116 | dirname = { optional=true, path="src/dirname" } 117 | du = { optional=true, path="src/du" } 118 | echo = { optional=true, path="src/echo" } 119 | env = { optional=true, path="src/env" } 120 | expand = { optional=true, path="src/expand" } 121 | expr = { optional=true, path="src/expr" } 122 | factor = { optional=true, path="src/factor" } 123 | false = { optional=true, path="src/false" } 124 | fmt = { optional=true, path="src/fmt" } 125 | fold = { optional=true, path="src/fold" } 126 | groups = { optional=true, path="src/groups" } 127 | hashsum = { optional=true, path="src/hashsum" } 128 | head = { optional=true, path="src/head" } 129 | hostid = { optional=true, path="src/hostid" } 130 | hostname = { optional=true, path="src/hostname" } 131 | id = { optional=true, path="src/id" } 132 | install = { optional=true, path="src/install" } 133 | kill = { optional=true, path="src/kill" } 134 | link = { optional=true, path="src/link" } 135 | ln = { optional=true, path="src/ln" } 136 | ls = { optional=true, path="src/ls" } 137 | logname = { optional=true, path="src/logname" } 138 | mkdir = { optional=true, path="src/mkdir" } 139 | mkfifo = { optional=true, path="src/mkfifo" } 140 | mknod = { optional=true, path="src/mknod" } 141 | mktemp = { optional=true, path="src/mktemp" } 142 | mv = { optional=true, path="src/mv" } 143 | nice = { optional=true, path="src/nice" } 144 | nl = { optional=true, path="src/nl" } 145 | nohup = { optional=true, path="src/nohup" } 146 | nproc = { optional=true, path="src/nproc" } 147 | od = { optional=true, path="src/od" } 148 | paste = { optional=true, path="src/paste" } 149 | pathchk = { optional=true, path="src/pathchk" } 150 | pinky = { optional=true, path="src/pinky" } 151 | printenv = { optional=true, path="src/printenv" } 152 | printf = { optional=true, path="src/printf" } 153 | ptx = { optional=true, path="src/ptx" } 154 | pwd = { optional=true, path="src/pwd" } 155 | readlink = { optional=true, path="src/readlink" } 156 | realpath = { optional=true, path="src/realpath" } 157 | relpath = { optional=true, path="src/relpath" } 158 | rm = { optional=true, path="src/rm" } 159 | rmdir = { optional=true, path="src/rmdir" } 160 | seq = { optional=true, path="src/seq" } 161 | shred = { optional=true, path="src/shred" } 162 | shuf = { optional=true, path="src/shuf" } 163 | sleep = { optional=true, path="src/sleep" } 164 | sort = { optional=true, path="src/sort" } 165 | split = { optional=true, path="src/split" } 166 | stat = { optional=true, path="src/stat" } 167 | stdbuf = { optional=true, path="src/stdbuf" } 168 | sum = { optional=true, path="src/sum" } 169 | sync = { optional=true, path="src/sync" } 170 | tac = { optional=true, path="src/tac" } 171 | tail = { optional=true, path="src/tail" } 172 | tee = { optional=true, path="src/tee" } 173 | test = { optional=true, path="src/test" } 174 | timeout = { optional=true, path="src/timeout" } 175 | touch = { optional=true, path="src/touch" } 176 | tr = { optional=true, path="src/tr" } 177 | true = { optional=true, path="src/true" } 178 | truncate = { optional=true, path="src/truncate" } 179 | tsort = { optional=true, path="src/tsort" } 180 | tty = { optional=true, path="src/tty" } 181 | uname = { optional=true, path="src/uname" } 182 | unexpand = { optional=true, path="src/unexpand" } 183 | uniq = { optional=true, path="src/uniq" } 184 | unlink = { optional=true, path="src/unlink" } 185 | uptime = { optional=true, path="src/uptime" } 186 | users = { optional=true, path="src/users" } 187 | wc = { optional=true, path="src/wc" } 188 | whoami = { optional=true, path="src/whoami" } 189 | yes = { optional=true, path="src/yes" } 190 | 191 | [dev-dependencies] 192 | time = "*" 193 | kernel32-sys = "*" 194 | winapi = "*" 195 | filetime = "*" 196 | libc = "*" 197 | memchr = "*" 198 | primal = "*" 199 | aho-corasick= "*" 200 | regex-syntax= "*" 201 | regex="*" 202 | rand="*" 203 | tempdir="*" 204 | 205 | [[bin]] 206 | name = "uutils" 207 | path = "src/uutils/uutils.rs" 208 | 209 | [[test]] 210 | name = "tests" 211 | -------------------------------------------------------------------------------- /test/error.toml: -------------------------------------------------------------------------------- 1 | # INVALID TOML DOC 2 | [[fruit]] 3 | name = "apple" 4 | 5 | [[fruit.variety]] 6 | name = "red delicious" 7 | 8 | # This table conflicts with the previous table 9 | [fruit.variety] 10 | name = "granny smith" 11 | -------------------------------------------------------------------------------- /test/example-v0.4.0.toml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## Comment 3 | 4 | # Speak your mind with the hash symbol. They go from the symbol to the end of 5 | # the line. 6 | 7 | 8 | ################################################################################ 9 | ## Table 10 | 11 | # Tables (also known as hash tables or dictionaries) are collections of 12 | # key/value pairs. They appear in square brackets on a line by themselves. 13 | 14 | [table] 15 | 16 | key = "value" # Yeah, you can do this. 17 | 18 | # Nested tables are denoted by table names with dots in them. Name your tables 19 | # whatever crap you please, just don't use #, ., [ or ]. 20 | 21 | [table.subtable] 22 | 23 | key = "another value" 24 | 25 | # You don't need to specify all the super-tables if you don't want to. TOML 26 | # knows how to do it for you. 27 | 28 | # [x] you 29 | # [x.y] don't 30 | # [x.y.z] need these 31 | [x.y.z.w] # for this to work 32 | 33 | [1.2] 34 | [1.2a] 35 | [a.2] 36 | [2.b] 37 | 38 | 39 | ################################################################################ 40 | ## Inline Table 41 | 42 | # Inline tables provide a more compact syntax for expressing tables. They are 43 | # especially useful for grouped data that can otherwise quickly become verbose. 44 | # Inline tables are enclosed in curly braces `{` and `}`. No newlines are 45 | # allowed between the curly braces unless they are valid within a value. 46 | 47 | [table.inline] 48 | 49 | name = { first = "Tom", last = "Preston-Werner" } 50 | point = { x = 1, y = 2 } 51 | 52 | 53 | ################################################################################ 54 | ## String 55 | 56 | # There are four ways to express strings: basic, multi-line basic, literal, and 57 | # multi-line literal. All strings must contain only valid UTF-8 characters. 58 | 59 | [string.basic] 60 | 61 | basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." 62 | 63 | [string.multiline] 64 | 65 | # The following strings are byte-for-byte equivalent: 66 | key1 = "One\nTwo" 67 | key2 = """One\nTwo""" 68 | key3 = """ 69 | One 70 | Two""" 71 | 72 | [string.multiline.continued] 73 | 74 | # The following strings are byte-for-byte equivalent: 75 | key1 = "The quick brown fox jumps over the lazy dog." 76 | 77 | key2 = """ 78 | The quick brown \ 79 | 80 | 81 | fox jumps over \ 82 | the lazy dog.""" 83 | 84 | key3 = """\ 85 | The quick brown \ 86 | fox jumps over \ 87 | the lazy dog.\ 88 | """ 89 | 90 | [string.literal] 91 | 92 | # What you see is what you get. 93 | winpath = 'C:\Users\nodejs\templates' 94 | winpath2 = '\\ServerX\admin$\system32\' 95 | quoted = 'Tom "Dubs" Preston-Werner' 96 | regex = '<\i\c*\s*>' 97 | 98 | 99 | [string.literal.multiline] 100 | 101 | regex2 = '''I [dw]on't need \d{2} apples''' 102 | lines = ''' 103 | The first newline is 104 | trimmed in raw strings. 105 | All other whitespace 106 | is preserved. 107 | ''' 108 | 109 | 110 | ################################################################################ 111 | ## Integer 112 | 113 | # Integers are whole numbers. Positive numbers may be prefixed with a plus sign. 114 | # Negative numbers are prefixed with a minus sign. 115 | 116 | [integer] 117 | 118 | key1 = +99 119 | key2 = 42 120 | key3 = 0 121 | key4 = -17 122 | 123 | [integer.underscores] 124 | 125 | # For large numbers, you may use underscores to enhance readability. Each 126 | # underscore must be surrounded by at least one digit. 127 | key1 = 1_000 128 | key2 = 5_349_221 129 | key3 = 1_2_3_4_5 # valid but inadvisable 130 | 131 | 132 | ################################################################################ 133 | ## Float 134 | 135 | # A float consists of an integer part (which may be prefixed with a plus or 136 | # minus sign) followed by a fractional part and/or an exponent part. 137 | 138 | [float.fractional] 139 | 140 | key1 = +1.0 141 | key2 = 3.1415 142 | key3 = -0.01 143 | 144 | [float.exponent] 145 | 146 | key1 = 5e+22 147 | key2 = 1e6 148 | key3 = -2E-2 149 | 150 | [float.both] 151 | 152 | key = 6.626e-34 153 | 154 | [float.underscores] 155 | 156 | key1 = 9_224_617.445_991_228_313 157 | key2 = 1e1_0_0 158 | 159 | 160 | ################################################################################ 161 | ## Boolean 162 | 163 | # Booleans are just the tokens you're used to. Always lowercase. 164 | 165 | [boolean] 166 | 167 | True = true 168 | False = false 169 | 170 | 171 | ################################################################################ 172 | ## Datetime 173 | 174 | # Datetimes are RFC 3339 dates. 175 | 176 | [datetime] 177 | 178 | key1 = 1979-05-27T07:32:00Z 179 | key2 = 1979-05-27T00:32:00-07:00 180 | key3 = 1979-05-27T00:32:00.999999-07:00 181 | key4 = 1979-05-27T00:32:00 182 | key5 = 1979-05-27 183 | 184 | 185 | ################################################################################ 186 | ## Array 187 | 188 | # Arrays are square brackets with other primitives inside. Whitespace is 189 | # ignored. Elements are separated by commas. Data types may not be mixed. 190 | 191 | [array] 192 | 193 | key1 = [ 1, 2, 3 ] 194 | key2 = [ "red", "yellow", "green" ] 195 | key3 = [ [ 1, 2 ], [3, 4, 5] ] 196 | key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok 197 | 198 | # Arrays can also be multiline. So in addition to ignoring whitespace, arrays 199 | # also ignore newlines between the brackets. Terminating commas are ok before 200 | # the closing bracket. 201 | 202 | key5 = [ 203 | 1, 2, 3 204 | ] 205 | key6 = [ 206 | 1, 207 | 2, # this is ok 208 | ] 209 | 210 | 211 | ################################################################################ 212 | ## Array of Tables 213 | 214 | # These can be expressed by using a table name in double brackets. Each table 215 | # with the same double bracketed name will be an element in the array. The 216 | # tables are inserted in the order encountered. 217 | 218 | [[products]] 219 | 220 | name = "Hammer" 221 | sku = 738594937 222 | 223 | [[products]] 224 | 225 | [[products]] 226 | 227 | name = "Nail" 228 | sku = 284758393 229 | color = "gray" 230 | 231 | 232 | # You can create nested arrays of tables as well. 233 | 234 | [[fruit]] 235 | name = "apple" 236 | 237 | [fruit.physical] 238 | color = "red" 239 | shape = "round" 240 | 241 | [[fruit.variety]] 242 | name = "red delicious" 243 | 244 | [[fruit.variety]] 245 | name = "granny smith" 246 | 247 | [[fruit]] 248 | name = "banana" 249 | 250 | [[fruit.variety]] 251 | name = "plantain" 252 | -------------------------------------------------------------------------------- /test/example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | 1a = "Bar key starting with number" 4 | title = "TOML Example" 5 | 6 | [owner] 7 | name = "Tom Preston-Werner" 8 | organization = "GitHub" 9 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 10 | 1-a = "Another bar key starting with number" 11 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 12 | 13 | [database] 14 | server = "192.168.1.1" 15 | ports = [ 8001, 8001, 8002 ] 16 | connection_max = 5000 17 | enabled = true 18 | 19 | [servers] 20 | 21 | # You can indent as you please. Tabs or spaces. TOML don't care. 22 | [servers.alpha] 23 | ip = "10.0.0.1" 24 | dc = "eqdc10" 25 | 26 | [servers.beta] 27 | ip = "10.0.0.2" 28 | dc = "eqdc10" 29 | country = "中国" # This should be parsed as UTF-8 30 | 31 | [clients] 32 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 33 | 34 | # Line breaks are OK when inside arrays 35 | hosts = [ 36 | "alpha", 37 | "omega" 38 | ] 39 | 40 | # Products 41 | 42 | [[products]] 43 | name = "Hammer" 44 | sku = 738594937 45 | 46 | [[products]] 47 | name = "Nail" 48 | sku = 284758393 49 | color = "gray" 50 | -------------------------------------------------------------------------------- /test/hard_example.toml: -------------------------------------------------------------------------------- 1 | # Test file for TOML 2 | # Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate 3 | # This part you'll really hate 4 | 5 | [the] 6 | test_string = "You'll hate me after this - #" # " Annoying, isn't it? 7 | 8 | [the.hard] 9 | test_array = [ "] ", " # "] # ] There you go, parse this! 10 | test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ] 11 | # You didn't think it'd as easy as chucking out the last #, did you? 12 | another_test_string = " Same thing, but with a string #" 13 | harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too" 14 | # Things will get harder 15 | 16 | [the.hard."bit#"] 17 | "what?" = "You don't think some user won't do that?" 18 | multi_line_array = [ 19 | "]", 20 | # ] Oh yes I did 21 | ] 22 | 23 | # Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test 24 | 25 | #[error] if you didn't catch this, your parser is broken 26 | #string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment" like this 27 | #array = [ 28 | # "This might most likely happen in multiline arrays", 29 | # Like here, 30 | # "or here, 31 | # and here" 32 | # ] End of array comment, forgot the # 33 | #number = 3.14 pi <--again forgot the # 34 | -------------------------------------------------------------------------------- /test/inf_in_keys.toml: -------------------------------------------------------------------------------- 1 | [info] 2 | this = "something" 3 | 4 | [inf] 5 | this = "something" 6 | 7 | [key1] 8 | inf = "something" 9 | 10 | [key2.inf] 11 | 12 | [nan.inf] 13 | -------------------------------------------------------------------------------- /test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'tomlrb' 3 | 4 | require 'minitest/autorun' 5 | require 'minitest/reporters' 6 | Minitest::Reporters.use! 7 | Minitest::Spec.make_my_diffs_pretty! 8 | -------------------------------------------------------------------------------- /test/nested_array_of_tables.toml: -------------------------------------------------------------------------------- 1 | [[fruit]] 2 | name = "apple" 3 | 4 | [fruit.physical] 5 | color = "red" 6 | shape = "round" 7 | 8 | [[fruit.variety]] 9 | name = "red delicious" 10 | 11 | [[fruit.variety]] 12 | name = "granny smith" 13 | 14 | [[fruit]] 15 | name = "banana" 16 | 17 | [fruit.physical] 18 | shape = "long" 19 | 20 | [[fruit.variety]] 21 | name = "plantain" 22 | -------------------------------------------------------------------------------- /test/test_compliance.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'pathname' 3 | require 'json' 4 | require 'yaml' 5 | 6 | EXPONENTIAL_NOTATIONS = %w[ 7 | values/spec-float-4.yaml 8 | values/spec-float-5.yaml 9 | values/spec-float-6.yaml 10 | values/spec-float-9.yaml 11 | ] 12 | 13 | UNDERSCORE_NUMBERS = %w[ 14 | values/spec-float-8.yaml 15 | ] 16 | 17 | RATIONAL_TIME = %w[ 18 | values/spec-date-time-3.yaml 19 | values/spec-date-time-5.yaml 20 | values/spec-date-time-6.yaml 21 | ] 22 | 23 | INT_KEY = %w[ 24 | values/spec-key-value-pair-9.yaml 25 | ] 26 | 27 | describe Tomlrb::Parser do 28 | tests_dirs = [ 29 | File.join(__dir__, '../toml-spec-tests'), 30 | File.join(__dir__, '../toml-test/tests') 31 | ] 32 | tests_dirs.each do |tests_dir| 33 | Pathname.glob("#{tests_dir}/{values,valid}/**/*.toml").each do |toml_path| 34 | toml_path = toml_path.expand_path 35 | local_path = toml_path.relative_path_from(Pathname.new(File.join(__dir__, '..'))) 36 | 37 | it "parses #{local_path}" do 38 | actual = Tomlrb.load_file(toml_path.to_path) 39 | json_path = toml_path.sub_ext('.json') 40 | yaml_path = toml_path.sub_ext('.yaml') 41 | expected = 42 | if json_path.exist? 43 | load_json(json_path) 44 | elsif yaml_path.exist? 45 | load_yaml(yaml_path) 46 | end 47 | _(actual).must_equal expected 48 | end 49 | end 50 | 51 | Pathname.glob("#{tests_dir}/{errors,invalid}/**/*.toml").each do |toml_path| 52 | toml_path = toml_path.expand_path 53 | local_path = toml_path.relative_path_from(Pathname.new(File.join(__dir__, '..'))) 54 | 55 | it "raises an error on parsing #{local_path}" do 56 | _{ Tomlrb.load_file(toml_path.to_path) }.must_raise Tomlrb::ParseError, RangeError, ArgumentError 57 | end 58 | end 59 | end 60 | end 61 | 62 | def process_json_leaf(node) 63 | v = node['value'] 64 | case node['type'] 65 | when 'string' 66 | v 67 | when 'integer' 68 | v.to_i 69 | when 'float' 70 | case v 71 | when 'inf', '+inf' 72 | Float::INFINITY 73 | when '-inf' 74 | -Float::INFINITY 75 | when 'nan' 76 | Float::NAN 77 | else 78 | v.to_f 79 | end 80 | when 'bool' 81 | case v 82 | when 'true' 83 | true 84 | when 'false' 85 | false 86 | end 87 | when 'datetime-local' 88 | date, time = v.split(/[t ]/i) 89 | year, month, day = date.split('-') 90 | hour, min, sec = time.split(':') 91 | Tomlrb::LocalDateTime.new(year, month, day, hour, min, sec.to_f) 92 | when 'date', 'date-local' 93 | Tomlrb::LocalDate.new(*v.split('-')) 94 | when 'time', 'time-local' 95 | hour, min, sec = v.split(':') 96 | Tomlrb::LocalTime.new(hour, min, sec.to_f) 97 | when 'datetime' 98 | if v.match?(/\.\d+/) 99 | date, time = v.split('T') 100 | year, month, day = date.split('-') 101 | hour, minute, second_and_zone = time.split(':', 3) 102 | second, frac_and_zone = second_and_zone.split('.') 103 | md = frac_and_zone.match(/(?\d+)(?.*)/) 104 | frac = md[:frac] 105 | zone = md[:zone] 106 | 107 | # Compatibility with Ruby 2.6 and lower 108 | if zone == 'Z' 109 | zone = '+00:00' 110 | end 111 | 112 | Time.new(year, month, day, hour, minute, "#{second}.#{frac}".to_f, zone) 113 | else 114 | Time.parse(v) 115 | end 116 | end 117 | end 118 | 119 | def process_json(node) 120 | case node 121 | when Hash 122 | if node['type'] 123 | process_json_leaf(node) 124 | else 125 | node.each_with_object({}) {|(key, value), table| 126 | table[key] = process_json(value) 127 | } 128 | end 129 | when Array 130 | node.map {|entry| process_json(entry)} 131 | else 132 | node 133 | end 134 | end 135 | 136 | def load_json(path) 137 | json = JSON.load(path.open) 138 | process_json(json) 139 | end 140 | 141 | def load_yaml(path) 142 | data = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(path.read) : YAML.load(path.read) 143 | local_path = path.parent.basename/path.basename 144 | if EXPONENTIAL_NOTATIONS.include? local_path.to_path 145 | data = data.each_with_object({}) {|(key, value), table| 146 | table[key] = value.to_f 147 | } 148 | end 149 | if UNDERSCORE_NUMBERS.include? local_path.to_path 150 | data = data.each_with_object({}) {|(key, value), table| 151 | table[key] = value.to_f 152 | } 153 | end 154 | if RATIONAL_TIME.include? local_path.to_path 155 | data = data.each_with_object({}) {|(key, value), table| 156 | sec_frac = value.to_f.to_s.split('.')[1] 157 | table[key] = Time.new(value.year, value.month, value.day, value.hour, value.min, "#{value.sec}.#{sec_frac}".to_f, value.utc_offset || value.zone) 158 | } 159 | end 160 | if INT_KEY.include? local_path.to_path 161 | data = data.each_with_object({}) {|(key, value), table| 162 | table[key.to_s] = value.each_with_object({}) {|(k, v), t| t[k.to_s] = v} 163 | } 164 | end 165 | 166 | data 167 | end 168 | -------------------------------------------------------------------------------- /test/test_key.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | describe Tomlrb::Key do 4 | subject { Tomlrb::Keys.new } 5 | 6 | it 'can define a super-table afterward' do 7 | subject.add_table_key %w[x y z w] 8 | subject.add_table_key %w[x] 9 | assert true 10 | end 11 | 12 | it 'cannot define a table more than once' do 13 | subject.add_table_key %w[fruit] 14 | subject.add_pair_key %w[apple], %w[fruit] 15 | _{ subject.add_table_key %w[fruit] }.must_raise Tomlrb::Key::KeyConflict 16 | end 17 | 18 | it 'cannot define a table more than once' do 19 | subject.add_table_key %w[fruit] 20 | subject.add_pair_key %w[apple], %w[fruit] 21 | _{ subject.add_table_key %w[fruit apple] }.must_raise Tomlrb::Key::KeyConflict 22 | end 23 | 24 | it 'can define tables out of order' do 25 | subject.add_table_key %w[fruit apple] 26 | subject.add_table_key %w[animal] 27 | subject.add_table_key %w[fruit orange] 28 | assert true 29 | end 30 | 31 | it 'is recommended to define tables in order' do 32 | subject.add_table_key %w[fruit apple] 33 | subject.add_table_key %w[fruit orange] 34 | subject.add_table_key %w[animal] 35 | assert true 36 | end 37 | 38 | it 'can define sub-tables within tables defined via dotted keys' do 39 | subject.add_table_key %w[fruit] 40 | subject.add_pair_key %w[apple color], %w[fruit] 41 | subject.add_pair_key %w[apple taste sweet], %w[fruit] 42 | subject.add_table_key %w[fruit apple texture] 43 | subject.add_pair_key %w[smooth], %w[fruit apple texture] 44 | assert true 45 | end 46 | 47 | it 'cannot define table key already defined as pair key' do 48 | subject.add_table_key %w[fruit] 49 | subject.add_pair_key %w[apple color], %w[fruit] 50 | subject.add_pair_key %w[apple taste sweet], %w[fruit] 51 | _{ subject.add_table_key %w[fruit apple] }.must_raise Tomlrb::Key::KeyConflict 52 | end 53 | 54 | it 'cannot define table key already defined as pair key' do 55 | subject.add_table_key %w[fruit] 56 | subject.add_pair_key %w[apple color], %w[fruit] 57 | subject.add_pair_key %w[apple taste sweet], %w[fruit] 58 | _{ subject.add_table_key %w[fruit apple taste] }.must_raise Tomlrb::Key::KeyConflict 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_local_date_time.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'tomlrb/local_date_time' 3 | require 'tomlrb/local_date' 4 | require 'tomlrb/local_time' 5 | 6 | def time_knows_zone_names? 7 | RUBY_VERSION >= '2.7.0' && RUBY_ENGINE != 'truffleruby' 8 | end 9 | 10 | describe Tomlrb::LocalDateTime do 11 | subject { Tomlrb::LocalDateTime } 12 | 13 | it 'keeps fractional part' do 14 | dt = subject.new('1979', '05', '27', '00', '32', 0.999999) 15 | _(dt.to_s).must_equal '1979-05-27T00:32:00.999999' 16 | end 17 | 18 | describe '#to_time' do 19 | subject { Tomlrb::LocalDateTime.new('1979', '05', '27', '00', '32', 0.999999) } 20 | 21 | it 'returns Time object with zero offset' do 22 | _(subject.to_time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, '+00:00') 23 | end 24 | 25 | it 'can change time zone by offset' do 26 | time = subject.to_time('+09:00') 27 | _(time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, '+09:00') 28 | end 29 | 30 | if time_knows_zone_names? 31 | it 'can change time zone by name' do 32 | time = subject.to_time('UTC') 33 | _(time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, 'UTC') 34 | end 35 | end 36 | end 37 | end 38 | 39 | describe Tomlrb::LocalDate do 40 | subject { Tomlrb::LocalDate } 41 | 42 | it 'can initialize with year, month and day' do 43 | d = subject.new('1979', '05', '27') 44 | _(d.to_s).must_equal '1979-05-27' 45 | end 46 | 47 | describe '#to_time' do 48 | subject { Tomlrb::LocalDate.new('1979', '05', '27') } 49 | 50 | it 'has 00:00:00 as time part' do 51 | time = subject.to_time 52 | _(time).must_equal Time.new(1979, 5, 27, 0, 0, 0, '+00:00') 53 | end 54 | 55 | it 'can change time zone by offset' do 56 | time = subject.to_time('+09:00') 57 | _(time).must_equal Time.new(1979, 5, 27, 0, 0, 0, '+09:00') 58 | end 59 | 60 | if time_knows_zone_names? 61 | it 'can change time zone by name' do 62 | time = subject.to_time('UTC') 63 | _(time).must_equal Time.new(1979, 5, 27, 0, 0, 0, 'UTC') 64 | end 65 | end 66 | end 67 | end 68 | 69 | describe Tomlrb::LocalTime do 70 | subject { Tomlrb::LocalTime } 71 | 72 | it 'keeps fractional part' do 73 | t = subject.new('00', '32', 0.999999) 74 | _(t.to_s).must_equal '00:32:00.999999' 75 | end 76 | 77 | describe '#to_time' do 78 | subject { Tomlrb::LocalTime.new('00', '32', 0.999999) } 79 | 80 | it 'requires date part' do 81 | time = subject.to_time(1979, 5, 27) 82 | _(time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, '+00:00') 83 | end 84 | 85 | it 'can change time zone with offset' do 86 | time = subject.to_time(1979, 5, 27, '+09:00') 87 | _(time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, '+09:00') 88 | end 89 | 90 | if time_knows_zone_names? 91 | it 'can change time zone with name' do 92 | time = subject.to_time(1979, 5, 27, 'UTC') 93 | _(time).must_equal Time.new(1979, 5, 27, 0, 32, 0.999999, 'UTC') 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/test_tomlrb.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | describe Tomlrb::Parser do 3 | it "parses a toml example file" do 4 | parsed_file = Tomlrb.load_file('./test/example.toml') 5 | _(parsed_file).must_equal TomlExamples.example 6 | end 7 | 8 | it "parses a toml v0.4.0 file" do 9 | parsed_file = Tomlrb.load_file('./test/example-v0.4.0.toml') 10 | _(parsed_file).must_equal TomlExamples.example_v_0_4_0 11 | end 12 | 13 | it "parses a toml hard file" do 14 | parsed_file = Tomlrb.load_file('./test/hard_example.toml') 15 | _(parsed_file).must_equal TomlExamples.hard_example 16 | end 17 | 18 | it "parses a Cargo file" do 19 | parsed_file = Tomlrb.load_file('./test/Cargo.toml') 20 | _(parsed_file).must_equal TomlExamples.cargo_example 21 | end 22 | 23 | it "parses tables whose keys include inf" do 24 | parsed_file = Tomlrb.load_file('./test/inf_in_keys.toml') 25 | _(parsed_file).must_equal TomlExamples.inf_in_keys_example 26 | end 27 | 28 | it "parses nested array of tables" do 29 | parsed_file = Tomlrb.load_file('./test/nested_array_of_tables.toml') 30 | _(parsed_file).must_equal TomlExamples.nested_array_of_tables 31 | end 32 | 33 | it "raises an error when defining a table with the same name as an already established array" do 34 | _{ Tomlrb.load_file('./test/error.toml') }.must_raise(Tomlrb::ParseError) 35 | end 36 | 37 | it "raises an error when parsing an unclosed table" do 38 | _{ Tomlrb.parse('''[[missingbracket]\na = 1''') }.must_raise(Tomlrb::ParseError) 39 | end 40 | 41 | it "does not fail with a false table value (GitHub issue #23)" do 42 | _( Tomlrb.parse("table=[ {name='name1', visible=true}, {name='name2', visible=false} ]") ) 43 | .must_equal({"table"=>[{"name"=>"name1", "visible"=>true}, {"name"=>"name2", "visible"=>false}]}) 44 | end 45 | 46 | it "symbolizes keys in a inline table if symbolize_keys: true" do 47 | _( Tomlrb.parse("table={a=1, b=2}", symbolize_keys: true) ) 48 | .must_equal({:table=>{:a=>1, :b=>2}}) 49 | end 50 | 51 | it "raises an error when parsing a float with leading underscore" do 52 | _{ Tomlrb.parse('x = _1.0') }.must_raise(Tomlrb::ParseError) 53 | end 54 | 55 | it "raises an error when parsing a float with trailing underscore" do 56 | _{ Tomlrb.parse('x = 2.0_') }.must_raise(Tomlrb::ParseError) 57 | end 58 | 59 | it "raises an error when parsing a float without integer before dot" do 60 | _{ Tomlrb.parse('x = .1') }.must_raise(Tomlrb::ParseError) 61 | end 62 | 63 | it "raises an error when parsing a float without integer behind dot" do 64 | _{ Tomlrb.parse('x = 0.') }.must_raise(Tomlrb::ParseError) 65 | end 66 | 67 | it "raises an error when parsing a float with leading 0, even in exponent" do 68 | _{ Tomlrb.parse('x = 01.2') }.must_raise(Tomlrb::ParseError) 69 | end 70 | 71 | it "raises an error when parsing table and others in a single line" do 72 | _{ Tomlrb.parse('[table] key = "value"') }.must_raise(Tomlrb::ParseError) 73 | end 74 | end 75 | 76 | class TomlExamples 77 | def self.example_v_0_4_0 78 | {"table"=>{"key"=>"value", "subtable"=>{"key"=>"another value"}, "inline"=>{"name"=>{"first"=>"Tom", "last"=>"Preston-Werner"}, "point"=>{"x"=>1, "y"=>2}}}, 79 | "x"=>{"y"=>{"z"=>{"w"=>{}}}}, 80 | "1"=>{"2"=>{},"2a"=>{}}, 81 | "a"=>{"2"=>{}}, 82 | "2"=>{"b"=>{}}, 83 | "string"=> 84 | {"basic"=>{"basic"=>"I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."}, 85 | "multiline"=> 86 | {"key1"=>"One\nTwo", 87 | "key2"=>"One\nTwo", 88 | "key3"=>"One\nTwo", 89 | "continued"=> 90 | {"key1"=>"The quick brown fox jumps over the lazy dog.", 91 | "key2"=>"The quick brown fox jumps over the lazy dog.", 92 | "key3"=>"The quick brown fox jumps over the lazy dog."}}, 93 | "literal"=> 94 | {"winpath"=>"C:\\Users\\nodejs\\templates", 95 | "winpath2"=>"\\\\ServerX\\admin$\\system32\\", 96 | "quoted"=>"Tom \"Dubs\" Preston-Werner", 97 | "regex"=>"<\\i\\c*\\s*>", 98 | "multiline"=>{"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"}}}, 99 | "integer"=>{"key1"=>99, "key2"=>42, "key3"=>0, "key4"=>-17, "underscores"=>{"key1"=>1000, "key2"=>5349221, "key3"=>12345}}, 100 | "float"=> 101 | {"fractional"=>{"key1"=>1.0, "key2"=>3.1415, "key3"=>-0.01}, 102 | "exponent"=>{"key1"=>5.0e+22, "key2"=>1000000.0, "key3"=>-0.02}, 103 | "both"=>{"key"=>6.626e-34}, 104 | "underscores"=>{"key1"=>9224617.445991227, "key2"=>1e1_0_0}}, 105 | "boolean"=>{"True"=>true, "False"=>false}, 106 | "datetime"=>{"key1"=>Time.utc(1979, 05, 27, 07, 32, 0), "key2"=>Time.new(1979, 05, 27, 00, 32, 0, '-07:00'), "key3"=>Time.new(1979, 05, 27, 00, 32, 0.999999, '-07:00'), "key4"=>Tomlrb::LocalDateTime.new(1979, 05, 27, 0, 32, 0.0), "key5"=>Tomlrb::LocalDate.new(1979, 05, 27)}, "array"=>{"key1"=>[1, 2, 3], "key2"=>["red", "yellow", "green"], "key3"=>[[1, 2], [3, 4, 5]], "key4"=>[[1, 2], ["a", "b", "c"]], "key5"=>[1, 2, 3], "key6"=>[1, 2]}, 107 | "products"=>[{"name"=>"Hammer", "sku"=>738594937}, {}, {"name"=>"Nail", "sku"=>284758393, "color"=>"gray"}], 108 | "fruit"=> 109 | [{"name"=>"apple", "physical"=>{"color"=>"red", "shape"=>"round"}, "variety"=>[{"name"=>"red delicious"}, {"name"=>"granny smith"}]}, 110 | {"name"=>"banana", "variety"=>[{"name"=>"plantain"}]}]} 111 | end 112 | 113 | def self.example 114 | { "1a" => "Bar key starting with number", 115 | "title"=>"TOML Example", 116 | "owner"=>{"name"=>"Tom Preston-Werner", "organization"=>"GitHub", "bio"=>"GitHub Cofounder & CEO\nLikes tater tots and beer.", "1-a" => "Another bar key starting with number", "dob"=>Time.parse('1979-05-27 07:32:00 +0000')}, 117 | "database"=>{"server"=>"192.168.1.1", "ports"=>[8001, 8001, 8002], "connection_max"=>5000, "enabled"=>true}, 118 | "servers"=>{"alpha"=>{"ip"=>"10.0.0.1", "dc"=>"eqdc10"}, "beta"=>{"ip"=>"10.0.0.2", "dc"=>"eqdc10", "country"=>"中国"}}, 119 | "clients"=>{"data"=>[["gamma", "delta"], [1, 2]], "hosts"=>["alpha", "omega"]}, 120 | "products"=>[{"name"=>"Hammer", "sku"=>738594937}, {"name"=>"Nail", "sku"=>284758393, "color"=>"gray"}]} 121 | end 122 | 123 | def self.hard_example 124 | {"the"=> 125 | {"test_string"=>"You'll hate me after this - #", 126 | "hard"=> 127 | {"test_array"=>["] ", " # "], 128 | "test_array2"=>["Test #11 ]proved that", "Experiment #9 was a success"], 129 | "another_test_string"=>" Same thing, but with a string #", 130 | "harder_test_string"=>" And when \"'s are in the string, along with # \"", 131 | "bit#"=> 132 | {"what?"=>"You don't think some user won't do that?", 133 | "multi_line_array"=>["]"]}}}} 134 | end 135 | 136 | def self.cargo_example 137 | {"package"=>{"name"=>"uutils", "version"=>"0.0.1", "authors"=>[], "build"=>"build.rs"}, "features"=>{"unix"=>["arch", "chmod", "chown", "chroot", "du", "groups", "hostid", "hostname", "id", "install", "kill", "logname", "mkfifo", "mknod", "mv", "nice", "nohup", "pathchk", "pinky", "stat", "stdbuf", "timeout", "touch", "tty", "uname", "unlink", "uptime", "users"], "generic"=>["base64", "basename", "cat", "cksum", "comm", "cp", "cut", "dircolors", "dirname", "echo", "env", "expand", "expr", "factor", "false", "fmt", "fold", "hashsum", "head", "link", "ln", "ls", "mkdir", "mktemp", "nl", "nproc", "od", "paste", "printenv", "printf", "ptx", "pwd", "readlink", "realpath", "relpath", "rm", "rmdir", "seq", "shred", "shuf", "sleep", "sort", "split", "sum", "sync", "tac", "tail", "tee", "test", "tr", "true", "truncate", "tsort", "unexpand", "uniq", "wc", "whoami", "yes"], "test_unimplemented"=>[], "nightly"=>[], "default"=>["generic", "unix"]}, "dependencies"=>{"uucore"=>{"path"=>"src/uucore"}, "arch"=>{"optional"=>true, "path"=>"src/arch"}, "base64"=>{"optional"=>true, "path"=>"src/base64"}, "basename"=>{"optional"=>true, "path"=>"src/basename"}, "cat"=>{"optional"=>true, "path"=>"src/cat"}, "chmod"=>{"optional"=>true, "path"=>"src/chmod"}, "chown"=>{"optional"=>true, "path"=>"src/chown"}, "chroot"=>{"optional"=>true, "path"=>"src/chroot"}, "cksum"=>{"optional"=>true, "path"=>"src/cksum"}, "comm"=>{"optional"=>true, "path"=>"src/comm"}, "cp"=>{"optional"=>true, "path"=>"src/cp"}, "cut"=>{"optional"=>true, "path"=>"src/cut"}, "dircolors"=>{"optional"=>true, "path"=>"src/dircolors"}, "dirname"=>{"optional"=>true, "path"=>"src/dirname"}, "du"=>{"optional"=>true, "path"=>"src/du"}, "echo"=>{"optional"=>true, "path"=>"src/echo"}, "env"=>{"optional"=>true, "path"=>"src/env"}, "expand"=>{"optional"=>true, "path"=>"src/expand"}, "expr"=>{"optional"=>true, "path"=>"src/expr"}, "factor"=>{"optional"=>true, "path"=>"src/factor"}, "false"=>{"optional"=>true, "path"=>"src/false"}, "fmt"=>{"optional"=>true, "path"=>"src/fmt"}, "fold"=>{"optional"=>true, "path"=>"src/fold"}, "groups"=>{"optional"=>true, "path"=>"src/groups"}, "hashsum"=>{"optional"=>true, "path"=>"src/hashsum"}, "head"=>{"optional"=>true, "path"=>"src/head"}, "hostid"=>{"optional"=>true, "path"=>"src/hostid"}, "hostname"=>{"optional"=>true, "path"=>"src/hostname"}, "id"=>{"optional"=>true, "path"=>"src/id"}, "install"=>{"optional"=>true, "path"=>"src/install"}, "kill"=>{"optional"=>true, "path"=>"src/kill"}, "link"=>{"optional"=>true, "path"=>"src/link"}, "ln"=>{"optional"=>true, "path"=>"src/ln"}, "ls"=>{"optional"=>true, "path"=>"src/ls"}, "logname"=>{"optional"=>true, "path"=>"src/logname"}, "mkdir"=>{"optional"=>true, "path"=>"src/mkdir"}, "mkfifo"=>{"optional"=>true, "path"=>"src/mkfifo"}, "mknod"=>{"optional"=>true, "path"=>"src/mknod"}, "mktemp"=>{"optional"=>true, "path"=>"src/mktemp"}, "mv"=>{"optional"=>true, "path"=>"src/mv"}, "nice"=>{"optional"=>true, "path"=>"src/nice"}, "nl"=>{"optional"=>true, "path"=>"src/nl"}, "nohup"=>{"optional"=>true, "path"=>"src/nohup"}, "nproc"=>{"optional"=>true, "path"=>"src/nproc"}, "od"=>{"optional"=>true, "path"=>"src/od"}, "paste"=>{"optional"=>true, "path"=>"src/paste"}, "pathchk"=>{"optional"=>true, "path"=>"src/pathchk"}, "pinky"=>{"optional"=>true, "path"=>"src/pinky"}, "printenv"=>{"optional"=>true, "path"=>"src/printenv"}, "printf"=>{"optional"=>true, "path"=>"src/printf"}, "ptx"=>{"optional"=>true, "path"=>"src/ptx"}, "pwd"=>{"optional"=>true, "path"=>"src/pwd"}, "readlink"=>{"optional"=>true, "path"=>"src/readlink"}, "realpath"=>{"optional"=>true, "path"=>"src/realpath"}, "relpath"=>{"optional"=>true, "path"=>"src/relpath"}, "rm"=>{"optional"=>true, "path"=>"src/rm"}, "rmdir"=>{"optional"=>true, "path"=>"src/rmdir"}, "seq"=>{"optional"=>true, "path"=>"src/seq"}, "shred"=>{"optional"=>true, "path"=>"src/shred"}, "shuf"=>{"optional"=>true, "path"=>"src/shuf"}, "sleep"=>{"optional"=>true, "path"=>"src/sleep"}, "sort"=>{"optional"=>true, "path"=>"src/sort"}, "split"=>{"optional"=>true, "path"=>"src/split"}, "stat"=>{"optional"=>true, "path"=>"src/stat"}, "stdbuf"=>{"optional"=>true, "path"=>"src/stdbuf"}, "sum"=>{"optional"=>true, "path"=>"src/sum"}, "sync"=>{"optional"=>true, "path"=>"src/sync"}, "tac"=>{"optional"=>true, "path"=>"src/tac"}, "tail"=>{"optional"=>true, "path"=>"src/tail"}, "tee"=>{"optional"=>true, "path"=>"src/tee"}, "test"=>{"optional"=>true, "path"=>"src/test"}, "timeout"=>{"optional"=>true, "path"=>"src/timeout"}, "touch"=>{"optional"=>true, "path"=>"src/touch"}, "tr"=>{"optional"=>true, "path"=>"src/tr"}, "true"=>{"optional"=>true, "path"=>"src/true"}, "truncate"=>{"optional"=>true, "path"=>"src/truncate"}, "tsort"=>{"optional"=>true, "path"=>"src/tsort"}, "tty"=>{"optional"=>true, "path"=>"src/tty"}, "uname"=>{"optional"=>true, "path"=>"src/uname"}, "unexpand"=>{"optional"=>true, "path"=>"src/unexpand"}, "uniq"=>{"optional"=>true, "path"=>"src/uniq"}, "unlink"=>{"optional"=>true, "path"=>"src/unlink"}, "uptime"=>{"optional"=>true, "path"=>"src/uptime"}, "users"=>{"optional"=>true, "path"=>"src/users"}, "wc"=>{"optional"=>true, "path"=>"src/wc"}, "whoami"=>{"optional"=>true, "path"=>"src/whoami"}, "yes"=>{"optional"=>true, "path"=>"src/yes"}}, "dev-dependencies"=>{"time"=>"*", "kernel32-sys"=>"*", "winapi"=>"*", "filetime"=>"*", "libc"=>"*", "memchr"=>"*", "primal"=>"*", "aho-corasick"=>"*", "regex-syntax"=>"*", "regex"=>"*", "rand"=>"*", "tempdir"=>"*"}, "bin"=>[{"name"=>"uutils", "path"=>"src/uutils/uutils.rs"}], "test"=>[{"name"=>"tests"}]} 138 | end 139 | 140 | def self.inf_in_keys_example 141 | { 142 | "info"=>{"this"=>"something"}, 143 | "inf"=>{"this"=>"something"}, 144 | "key1"=>{"inf"=>"something"}, 145 | "key2"=>{"inf"=>{}}, 146 | "nan"=>{"inf"=>{}} 147 | } 148 | end 149 | 150 | def self.nested_array_of_tables 151 | { 152 | "fruit"=> 153 | [ 154 | { 155 | "name" => "apple", 156 | "physical" => { 157 | "color" => "red", 158 | "shape" => "round" 159 | }, 160 | "variety" => [ 161 | {"name" => "red delicious"}, 162 | {"name" => "granny smith"} 163 | ] 164 | }, 165 | { 166 | "name" => "banana", 167 | "physical" => {"shape" => "long"}, 168 | "variety" => [ 169 | {"name" => "plantain"} 170 | ] 171 | } 172 | ] 173 | } 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /tomlrb.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tomlrb/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "tomlrb" 8 | spec.version = Tomlrb::VERSION 9 | spec.authors = ["Francois Bernier"] 10 | spec.email = ["frankbernier@gmail.com"] 11 | 12 | spec.summary = %q{A racc based toml parser} 13 | spec.description = %q{A racc based toml parser} 14 | spec.homepage = "https://github.com/fbernier/tomlrb" 15 | spec.license = "MIT" 16 | 17 | spec.files = %w{LICENSE.txt} + Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } 18 | spec.require_paths = ["lib"] 19 | spec.required_ruby_version = '>= 2.0' 20 | 21 | spec.add_development_dependency "psych", "~> 4" 22 | end 23 | --------------------------------------------------------------------------------