├── .gitignore ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── bin ├── console └── setup ├── figtree.gemspec ├── lib ├── figtree.rb └── figtree │ ├── ini_config.rb │ ├── ip_rules.rb │ ├── parser.rb │ ├── transformer.rb │ └── version.rb └── spec ├── figtree ├── parser_spec.rb └── transformer_spec.rb ├── figtree_spec.rb ├── spec_helper.rb └── support ├── comment.ini ├── good.ini ├── invalid ├── bad_1.ini ├── bad_2.ini ├── browscap.ini ├── continuation.ini ├── escape.ini ├── global.ini ├── unparseable_settings.conf └── untransformable_settings.conf ├── merge.ini ├── mini_ini_example.ini ├── mixed_comment.ini ├── multiline.ini ├── multiline_only.ini ├── param.ini ├── section.ini ├── settings.conf └── wiki_example.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | *.gem 4 | *.rbc 5 | /.config 6 | /coverage/ 7 | /InstalledFiles 8 | /pkg/ 9 | /spec/reports/ 10 | /spec/examples.txt 11 | /test/tmp/ 12 | /test/version_tmp/ 13 | /tmp/ 14 | 15 | ## Specific to RubyMotion: 16 | .dat* 17 | .repl_history 18 | build/ 19 | 20 | ## Documentation cache and generated files: 21 | /.yardoc/ 22 | /_yardoc/ 23 | /doc/ 24 | /rdoc/ 25 | 26 | ## Environment normalisation: 27 | /.bundle/ 28 | /vendor/bundle 29 | /lib/bundler/man/ 30 | 31 | # for a library or gem, you might want to ignore these files since the code is 32 | # intended to run in multiple environments; otherwise, check them in: 33 | # Gemfile.lock 34 | # .ruby-version 35 | # .ruby-gemset 36 | 37 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 38 | .rvmrc 39 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.2.2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - Version 0.0.1: initial set up 2 | - Version 0.0.2: some bug fixes, overrides 3 | - Version 1.0.0: moving away from #load_conifg to just #new 4 | - Version 1.0.1: refactoring in IniConfig 5 | - Version 1.2.0: adding support for pre-group comments and ip addresses 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | figtree (2.0.0) 5 | parslet (~> 1.7) 6 | wannabe_bool (~> 0.2) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | blankslate (3.1.3) 12 | coderay (1.1.0) 13 | diff-lcs (1.2.5) 14 | docile (1.1.5) 15 | json (1.8.3) 16 | method_source (0.8.2) 17 | parslet (1.7.1) 18 | blankslate (>= 2.0, <= 4.0) 19 | pry (0.10.1) 20 | coderay (~> 1.1.0) 21 | method_source (~> 0.8.1) 22 | slop (~> 3.4) 23 | rspec (3.3.0) 24 | rspec-core (~> 3.3.0) 25 | rspec-expectations (~> 3.3.0) 26 | rspec-mocks (~> 3.3.0) 27 | rspec-core (3.3.2) 28 | rspec-support (~> 3.3.0) 29 | rspec-expectations (3.3.1) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.3.0) 32 | rspec-mocks (3.3.2) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.3.0) 35 | rspec-support (3.3.0) 36 | simplecov (0.10.0) 37 | docile (~> 1.1.0) 38 | json (~> 1.8) 39 | simplecov-html (~> 0.10.0) 40 | simplecov-html (0.10.0) 41 | slop (3.6.0) 42 | wannabe_bool (0.2.0) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | figtree! 49 | pry (~> 0.10) 50 | rspec (~> 3.0, >= 3.0.0) 51 | simplecov (~> 0.10) 52 | 53 | BUNDLED WITH 54 | 1.10.6 55 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Moore - Niemi 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 | %%%,%%%%%%% 2 | ,'%% \\-*%%%%%%% 3 | ;%%%%%*% _%%%%" 4 | ,%%% \(_.*%%%%. 5 | % *%%, ,%%%%*( ' 6 | %^ ,*%%% )\|,%%*%,_ 7 | *% \/ #).-"*%%* 8 | _.) ,/ *%, 9 | _________/)#(_____________ 10 | #### art by [b'ger](http://ascii.co.uk/art/tree) 11 | 12 | # Figtree 13 | ## about 14 | A parser and transformer for loading `.ini` files into Ruby dot notation accessible objects. `.ini` is not a standardized format. But the parser and transformer are easy to extend. 15 | 16 | The goal of figtree is not to accept all `.ini` files generously, but more strictly define `.ini` files so we can make smarter guesses about how to convert your settings into objects. 17 | 18 | What kinds of objects are supported? Currently we can recognize [unix style filepaths into Pathname](http://ruby-doc.org/stdlib-2.0.0/libdoc/pathname/rdoc/Pathname.html), [ip4 and ip6 addresses into IPAddr](http://ruby-doc.org/stdlib-2.0.0/libdoc/ipaddr/rdoc/IPAddr.html), and most common Ruby types (String, Array, Boolean, Integer). If there's other types you'd like to see supported, please [file an issue](https://github.com/mooreniemi/figtree/issues/new). 19 | 20 | If the `.ini` file is invalid, an error will be raised, with the line and char position of the error. If you extend this gem to have more rules, and one of those rules fails to transform, you will have an error raised. 21 | 22 | ## alternatives 23 | If you want an industrial strength, pure Ruby solution, check out [inifile gem](https://github.com/TwP/inifile). It is much looser about what it accepts as valid `.ini` files, and with no pesky dependencies! If you want to see exactly which cases `figtree` supports vs `inifile`, compare our spec files. Many of the cases in our `invalid/` folder `inifile` will parse for you. 24 | 25 | ## installation 26 | `gem install figtree` 27 | 28 | ## usage 29 | require 'figtree' 30 | config = Figtree::IniConfig.new('spec/support/settings.conf') 31 | config.common.basic_size_limit 32 | => 26214400 33 | # also good 34 | config[:common]["paid_users_size_limit"] 35 | => 2147483648 36 | # also also good :) 37 | config.common[:paid_users_size_limit] 38 | => 2147483648 39 | # and overrides? we got overrides 40 | overridden_config = Figtree::IniConfig.new('spec/support/settings.conf', :production) 41 | config.ftp.path 42 | => "/tmp/" 43 | overridden_config.ftp.path 44 | => "/srv/var/tmp/" 45 | 46 | ## disambiguation 47 | Looking for the graphical viewer of phyllogenic trees? You want this other [Figtree](http://tree.bio.ed.ac.uk/software/figtree/). 48 | 49 | ## performance 50 | A typical `.ini` file takes slightly less than 0.02s to be parsed, transformed, and loaded. Currently, the whole `.ini` file is read into memory at once. The assumption being these files should not typically be too big. But future minor versions might move to line by line ingestion. 51 | 52 | ## development 53 | ### helpers 54 | `bin/setup` 55 | `bin/console` 56 | 57 | While in console, you can use `parse_ini_named name` and as long as it's in `spec/support/name.ini` it'll parse it for you with helpful error output. Sometimes this is a faster feedback mechanism than going through the tests. 58 | 59 | ### tests 60 | `rspec spec/` 61 | 62 | #### TODO 63 | - more cleanup 64 | - give char/line position of transformer failures 65 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "figtree" 5 | 6 | require "parslet/convenience" 7 | 8 | parser = Figtree::Parser.new 9 | 10 | require "pry" 11 | command_set = Pry::CommandSet.new do 12 | command "parse_ini_named", "", :keep_retval => true do |name| 13 | parser = Figtree::Parser.new 14 | ini_as_string = File.read("spec/support/#{name}.ini") 15 | parser.parse_with_debug(ini_as_string) 16 | end 17 | end 18 | 19 | #Pry.start parser, :commands => command_set 20 | Pry.start nil, :commands => command_set 21 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | 9 | -------------------------------------------------------------------------------- /figtree.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "figtree/version" 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = 'figtree' 6 | gem.version = Figtree::VERSION 7 | gem.date = Date.today.to_s 8 | gem.licenses = ['MIT'] 9 | 10 | gem.summary = "A parser and transformer for loading `.ini` files into Ruby dot notation accessible objects." 11 | gem.description = "See README.md" 12 | 13 | gem.authors = ['Alex Moore-Niemi'] 14 | gem.email = 'moore.niemi@gmail.com' 15 | gem.homepage = 'https://github.com/mooreniemi/figtree' 16 | 17 | gem.add_runtime_dependency 'parslet', '~> 1.7' 18 | gem.add_runtime_dependency 'wannabe_bool', '~> 0.2' 19 | gem.add_development_dependency 'rspec', '~> 3.0', '>= 3.0.0' 20 | gem.add_development_dependency 'simplecov', '~> 0.10' 21 | gem.add_development_dependency 'pry', '~> 0.10' 22 | 23 | gem.files = `git ls-files`.split("\n") 24 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 25 | gem.require_paths = ["lib"] 26 | end 27 | -------------------------------------------------------------------------------- /lib/figtree.rb: -------------------------------------------------------------------------------- 1 | require 'figtree/parser' 2 | require 'figtree/transformer' 3 | require 'figtree/ini_config' 4 | -------------------------------------------------------------------------------- /lib/figtree/ini_config.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | module Figtree 3 | class IniConfig < OpenStruct 4 | def initialize(ini, override = :none) 5 | # cheat to allow a parsed hash in 6 | parsed_subgroups = ini.is_a?(Hash) ? 7 | ini : subgroups_from(ini, override) 8 | super(parsed_subgroups) 9 | end 10 | 11 | private 12 | def subgroups_from(ini_file, override) 13 | figgy_transform( 14 | figgy_parse( 15 | File.read(ini_file) 16 | ), 17 | override 18 | ).reduce({}, :merge!) 19 | end 20 | 21 | def figgy_parse(str) 22 | Parser.new.parse(str) 23 | # argument error is invalid byte sequence 24 | rescue Parslet::ParseFailed, ArgumentError => failure 25 | if failure.class == Parslet::ParseFailed 26 | failure = failure.cause.ascii_tree 27 | end 28 | STDERR.puts "\nInvalid ini file.\n" + 29 | "Error: #{failure}" + 30 | "Please correct the file and retry." 31 | raise 32 | end 33 | 34 | def figgy_transform(tree, override) 35 | Transformer.new.apply(tree, override: override) 36 | rescue => e 37 | STDERR.puts "\nInvalid transformation rule.\n" + 38 | "Error: #{e}" + 39 | "Please correct your transformer rule and retry." 40 | raise TransformFailed 41 | end 42 | end 43 | 44 | class Subgroup < OpenStruct 45 | end 46 | 47 | class TransformFailed < Exception 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/figtree/ip_rules.rb: -------------------------------------------------------------------------------- 1 | # from https://github.com/kschiess/parslet/blob/master/example/ip_address.rb 2 | module Figtree 3 | # Must be used in concert with IPv4 4 | module IPv6 5 | include Parslet 6 | 7 | rule(:colon) { str(':') } 8 | rule(:dcolon) { colon >> colon } 9 | 10 | # h16 : 11 | def h16r(times) 12 | (h16 >> colon).repeat(times, times) 13 | end 14 | 15 | # : h16 16 | def h16l(times) 17 | (colon >> h16).repeat(0,times) 18 | end 19 | 20 | # A 128-bit IPv6 address is divided into eight 16-bit pieces. Each piece is 21 | # represented numerically in case-insensitive hexadecimal, using one to four 22 | # hexadecimal digits (leading zeroes are permitted). The eight encoded 23 | # pieces are given most-significant first, separated by colon characters. 24 | # Optionally, the least-significant two pieces may instead be represented in 25 | # IPv4 address textual format. A sequence of one or more consecutive 26 | # zero-valued 16-bit pieces within the address may be elided, omitting all 27 | # their digits and leaving exactly two consecutive colons in their place to 28 | # mark the elision. 29 | rule(:ipv6) { 30 | ( 31 | ( 32 | h16r(6) | 33 | dcolon >> h16r(5) | 34 | h16.maybe >> dcolon >> h16r(4) | 35 | (h16 >> h16l(1)).maybe >> dcolon >> h16r(3) | 36 | (h16 >> h16l(2)).maybe >> dcolon >> h16r(2) | 37 | (h16 >> h16l(3)).maybe >> dcolon >> h16r(1) | 38 | (h16 >> h16l(4)).maybe >> dcolon 39 | ) >> ls32 | 40 | (h16 >> h16l(5)).maybe >> dcolon >> h16 | 41 | (h16 >> h16l(6)).maybe >> dcolon 42 | ).as(:ipv6) 43 | } 44 | 45 | rule(:h16) { 46 | hexdigit.repeat(1,4) 47 | } 48 | 49 | rule(:ls32) { 50 | (h16 >> colon >> h16) | 51 | ipv4 52 | } 53 | 54 | rule(:hexdigit) { 55 | digit | match("[a-fA-F]") 56 | } 57 | end 58 | 59 | module IPv4 60 | include Parslet 61 | 62 | # A host identified by an IPv4 literal address is represented in 63 | # dotted-decimal notation (a sequence of four decimal numbers in the range 0 64 | # to 255, separated by "."), as described in [RFC1123] by reference to 65 | # [RFC0952]. Note that other forms of dotted notation may be interpreted on 66 | # some platforms, as described in Section 7.4, but only the dotted-decimal 67 | # form of four octets is allowed by this grammar. 68 | rule(:ipv4) { 69 | (dec_octet >> str('.') >> dec_octet >> str('.') >> 70 | dec_octet >> str('.') >> dec_octet).as(:ipv4) 71 | } 72 | 73 | rule(:dec_octet) { 74 | str('25') >> match("[0-5]") | 75 | str('2') >> match("[0-4]") >> digit | 76 | str('1') >> digit >> digit | 77 | match('[1-9]') >> digit | 78 | digit 79 | } 80 | 81 | rule(:digit) { 82 | match('[0-9]') 83 | } 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/figtree/parser.rb: -------------------------------------------------------------------------------- 1 | require 'parslet' 2 | require 'figtree/ip_rules' 3 | 4 | module Figtree 5 | # ConFIG into a Tree :) 6 | class Parser < Parslet::Parser 7 | include IPv4 8 | include IPv6 9 | 10 | rule(:eof) { any.absent? } 11 | rule(:group_title) { match('[a-zA-Z_]').repeat(1) } 12 | rule(:space) { (match("\s") | str(' ')) } 13 | rule(:newline) { str("\n") >> match("\r").maybe } 14 | rule(:terminator) do 15 | space.repeat(0) >> (comment | newline | eof) 16 | end 17 | rule(:backslash) do 18 | space.repeat(0) >> str("\\") 19 | end 20 | 21 | rule(:grouper) do 22 | str('[') >> 23 | group_title.as(:group_title) >> 24 | str(']') 25 | end 26 | 27 | rule(:comment_start) { (str(';') | str('#')) } 28 | rule(:comment_end) { (newline | eof) } 29 | rule(:comment) do 30 | ( 31 | comment_start >> 32 | space.repeat(0) >> 33 | ( 34 | comment_end.absent? >> any 35 | ).repeat 36 | ) >> 37 | space.repeat(0) >> 38 | comment_end 39 | end 40 | 41 | rule(:quoted_string) do 42 | str('"') >> 43 | ( 44 | (str('\\') >> any) | (str('"').absent? >> any) 45 | ).repeat(1) >> 46 | str('"') 47 | end 48 | 49 | 50 | rule(:unquoted_string) do 51 | ( 52 | ( 53 | ( 54 | (backslash | terminator).absent? 55 | ) >> any 56 | ).repeat(1).as(:left) >> 57 | backslash >> 58 | terminator 59 | ).repeat(0) >> 60 | ( 61 | terminator.absent? >> any 62 | ).repeat(1).as(:right) >> 63 | terminator 64 | end 65 | 66 | rule(:string) do 67 | (quoted_string | unquoted_string).as(:string) 68 | end 69 | 70 | rule(:boolean) do 71 | ( 72 | str('no') | 73 | str('yes') | 74 | str('false') | 75 | str('true') 76 | ).as(:boolean) 77 | end 78 | 79 | rule(:number) do 80 | match('[0-9]').repeat(1).as(:number) 81 | end 82 | 83 | rule(:ip_address) do 84 | (ipv4 | ipv6).as(:ip_address) 85 | end 86 | 87 | rule(:at_least_one_char) do 88 | match('[a-zA-Z]').repeat(1) 89 | end 90 | 91 | rule(:array) do 92 | ( 93 | # minimum array 94 | at_least_one_char >> 95 | ( 96 | # extending elementwise 97 | str(',') >> space.repeat.maybe >> 98 | at_least_one_char 99 | ).repeat(1) 100 | ).as(:array) >> 101 | (newline | eof) 102 | end 103 | 104 | rule(:file_path) do 105 | ( 106 | ( 107 | str('/') >> 108 | at_least_one_char 109 | ).repeat(1) >> 110 | str('/').maybe 111 | ).as(:file_path) 112 | end 113 | 114 | rule(:snake_case_key) do 115 | match('[a-zA-Z0-9_]').repeat(1). 116 | as(:snake_case_key) 117 | end 118 | 119 | rule(:snakey_option_key) do 120 | snake_case_key.as(:key_to_be_overridden) >> 121 | str('<') >> 122 | snake_case_key.as(:optional_key) >> 123 | str('>') 124 | end 125 | 126 | rule(:value) do 127 | # this ordering matters 128 | # we are roughly moving from more 129 | # to less specific 130 | ( 131 | ip_address | 132 | number | 133 | boolean | 134 | array | 135 | file_path | 136 | string 137 | ) 138 | end 139 | 140 | rule(:equals_value) do 141 | space.repeat(0) >> 142 | str("=") >> 143 | space.repeat(0) >> 144 | value >> 145 | newline.repeat(0) 146 | end 147 | 148 | rule(:assignment) do 149 | snake_case_key >> 150 | equals_value 151 | end 152 | 153 | rule(:override_assignment) do 154 | snakey_option_key >> 155 | equals_value 156 | end 157 | 158 | rule(:assignment_or_comment) do 159 | ( comment | assignment | override_assignment ) 160 | end 161 | 162 | rule(:group_member) do 163 | assignment_or_comment >> 164 | space.repeat(0) >> 165 | newline.repeat(0) 166 | end 167 | 168 | rule(:group) do 169 | ( 170 | grouper >> 171 | space.repeat(0) >> 172 | comment.maybe >> 173 | newline.repeat(0) >> 174 | group_member.repeat(0) 175 | ).as(:group). 176 | repeat(0) 177 | end 178 | 179 | rule(:comment_or_group) do 180 | # may start file with attribution 181 | # comment or timestamp etc 182 | comment.repeat.maybe >> 183 | group 184 | end 185 | 186 | root(:comment_or_group) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/figtree/transformer.rb: -------------------------------------------------------------------------------- 1 | require 'parslet' 2 | require 'ostruct' 3 | require 'ipaddr' 4 | require 'wannabe_bool' 5 | 6 | module Figtree 7 | # a transformer takes a parsed, valid AST and applies rules, usually 8 | # in a context free manner 9 | class Transformer < Parslet::Transform 10 | rule(:snake_case_key => simple(:key), :number => simple(:value)) do 11 | { 12 | key.to_sym => Integer(value) 13 | } 14 | end 15 | rule(:snake_case_key => simple(:key), :string => subtree(:value)) do 16 | merged_string = 17 | case value 18 | when Hash 19 | value[:right] 20 | when Array 21 | value.inject("") do |string, element| 22 | if !element[:left].nil? 23 | string + element[:left] 24 | else 25 | string + element[:right] 26 | end 27 | end 28 | else 29 | value 30 | end 31 | { 32 | # remove whitespace after cast 33 | key.to_sym => String(merged_string).strip 34 | } 35 | end 36 | rule(:snake_case_key => simple(:key), :file_path => simple(:value)) do 37 | { 38 | key.to_sym => Pathname.new(value) 39 | } 40 | end 41 | # depends on wannabe_bool refining String class 42 | rule(:snake_case_key => simple(:key), :boolean => simple(:value)) do 43 | { 44 | key.to_sym => String(value).to_b 45 | } 46 | end 47 | rule(:snake_case_key => simple(:key), :array => simple(:value)) do 48 | { 49 | key.to_sym => String(value).split(",") 50 | } 51 | end 52 | 53 | rule(:snake_case_key => simple(:key), :ip_address => subtree(:value)) do 54 | { 55 | key.to_sym => IPAddr.new((value.values.first.to_s)) 56 | } 57 | end 58 | 59 | # ini files are trees of a fixed height, if the file handle is the root 60 | # subgroups are its children, and subgroup members are the next level of children 61 | rule(:group => subtree(:group_members)) do 62 | group_title = group_members[0][:group_title].to_sym 63 | group_values = Subgroup.new(group_members[1..-1].reduce({}, :merge!)) 64 | { 65 | group_title => group_values 66 | } 67 | end 68 | 69 | # where does overrides come from? an argument into #apply on 70 | # Transformer, that allows an additional capture outside the AST 71 | # to be added to the context of the transform 72 | rule( 73 | :key_to_be_overridden => subtree(:overridden_key), 74 | :optional_key => subtree(:overriding_key), 75 | :file_path => subtree(:new_file_path), 76 | ) do 77 | if override.to_sym == overriding_key[:snake_case_key].to_sym 78 | { 79 | overridden_key[:snake_case_key] => Pathname.new(new_file_path) 80 | } 81 | else 82 | { 83 | } 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/figtree/version.rb: -------------------------------------------------------------------------------- 1 | module Figtree 2 | # for reference see 3 | # http://guides.rubygems.org/patterns/#semantic-versioning 4 | VERSION = "2.0.1" 5 | end 6 | -------------------------------------------------------------------------------- /spec/figtree/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Figtree 4 | describe Parser do 5 | let(:parser) { Parser.new } 6 | let(:string) do 7 | "a bb ccc dddd eeeee ffffff\n" 8 | end 9 | 10 | it 'can parse newlines' do 11 | expect(parser.newline).to parse("\n") 12 | expect(parser.newline).to_not parse("\\n") 13 | expect(parser.newline).to_not parse("\\\n") 14 | end 15 | it 'terminates correctly' do 16 | expect(parser.terminator).to parse(" #comment") 17 | expect(parser.terminator).to parse(" #comment\n") 18 | expect(parser.terminator).to parse(" \n") 19 | expect(parser.terminator).to parse(" ") 20 | expect(parser.unquoted_string).to parse("f #ffoo") 21 | expect(parser.unquoted_string).to parse("f #ffoo\n") 22 | end 23 | it 'can parse group names' do 24 | expect(parser.grouper).to parse('[common]') 25 | expect(parser.grouper).to parse('[common_also]') 26 | end 27 | it 'can parse comments' do 28 | expect(parser.comment).to parse("; This is a comment\n") 29 | expect(parser.comment).to parse("# This is also a comment\n") 30 | expect(parser.comment).to parse("; last modified 1 April 2001 by John Doe\n") 31 | expect(parser.comment).to parse("#comment \\n") 32 | comment_first = File.open('spec/support/wiki_example.ini', &:readline) 33 | expect(parser.comment).to parse(comment_first) 34 | end 35 | it 'can parse snake_case keys' do 36 | expect(parser.snake_case_key).to parse('basic_size_limit') 37 | end 38 | 39 | describe 'strings of all kinds' do 40 | it 'can parse strings' do 41 | expect(parser.string).to parse('"hello there, ftp uploading"') 42 | end 43 | it 'can parse unquoted strings' do 44 | expect(parser.unquoted_string).to parse(string) 45 | expect(parser.unquoted_string).to parse("multiline \\\nsupport\n") 46 | end 47 | it 'can parse multiline with comment' do 48 | expect(parser.unquoted_string).to parse("a #comment\n") 49 | expect(parser.unquoted_string).to parse("a #") 50 | expect(parser.unquoted_string).to_not parse("a\nb") 51 | expect(parser.unquoted_string).to parse("a \\#comment\n b\n") 52 | expect(parser.assignment).to parse("foo = a \\nb\n") 53 | expect(parser.assignment).to parse("foo = a \\ # and here, too\nb\n") 54 | 55 | group = "[section_three]\nthree = hello \\\nmultiline\nother = 2" 56 | expect(parser.group).to parse(group) 57 | 58 | multiline_only = File.read('spec/support/multiline_only.ini') 59 | expect(parser.group).to parse(multiline_only) 60 | end 61 | end 62 | 63 | it 'can parse arrays' do 64 | expect(parser.array).to_not parse(',,') 65 | expect(parser.array).to_not parse("a\n") 66 | expect(parser.array).to_not parse("a,") 67 | expect(parser.array).to parse("a,b\n") 68 | expect(parser.array).to parse("a,b") 69 | expect(parser.array).to parse("a,b,c\n") 70 | expect(parser.array).to parse("words, with, spaces, after\n") 71 | expect(parser.array).to parse("several,diff,words,only,nonumbers\n") 72 | expect(parser.array).to parse("array,of,values\n") 73 | expect(parser.array).to parse("array,of,values") 74 | end 75 | 76 | it 'can parse numbers' do 77 | expect(parser.number).to parse("26214400") 78 | end 79 | 80 | it 'can parse ip addresses' do 81 | expect(parser.ip_address).to parse("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") 82 | expect(parser.ip_address).to parse('111.222.3.4') 83 | expect(parser.ip_address).to parse('192.0.2.62') 84 | expect(parser.ip_address).to_not parse('f11.222.3.4') 85 | expect(parser.ip_address).to_not parse('111.222.3') 86 | end 87 | it 'can parse booleans flexibly' do 88 | expect(parser.boolean).to parse("no") 89 | expect(parser.boolean).to parse("yes") 90 | expect(parser.boolean).to parse("true") 91 | expect(parser.boolean).to parse("false") 92 | end 93 | it 'can parse file_paths' do 94 | expect(parser.file_path).to parse('/srv/tmp/') 95 | expect(parser.file_path).to_not parse(string) 96 | end 97 | 98 | describe 'overriding stuff' do 99 | it 'can parse keys with optional overrides' do 100 | expect(parser.snakey_option_key).to parse('path') 101 | end 102 | it 'can parse overrides' do 103 | expect(parser.override_assignment).to parse('path = /srv/tmp/') 104 | end 105 | end 106 | 107 | describe "building up from root, checking group, etc" do 108 | let(:settings_path) { 'spec/support/settings.conf' } 109 | let(:multi_group) { 110 | [ 111 | "[common]", 112 | "basic_size_limit = 234234", 113 | "[rare]", 114 | "pepes = 0", 115 | "and = feels guy" 116 | ].join("\n") + "\n" 117 | } 118 | 119 | it 'can parse assignments' do 120 | expect(parser.assignment).to parse('basic_size_limit = 26214400') 121 | expect(parser.assignment).to parse('path = /srv/var/tmp/') 122 | expect(parser.assignment).to_not parse('path = /srv/tmp/') 123 | expect(parser.assignment).to parse('name = "hello there, ftp uploading"') 124 | expect(parser.assignment).to parse('params = array,of,values') 125 | expect(parser.assignment).to parse("hostname = My Computer\n") 126 | end 127 | it 'can parse comments then groups' do 128 | expect(parser.comment_or_group).to parse("; comment\n[groop]\nassignment = 0\n") 129 | end 130 | it 'parses comment or group' do 131 | expect(parser.comment_or_group).to parse("[database]\nserver = 192.0.2.62") 132 | end 133 | it 'parses a group member' do 134 | expect(parser.group_member).to parse("basic_size_limit = 26214400\n") 135 | end 136 | it 'can parse group members with inline comments' do 137 | group_with_comments = "[section_two] # you can comment here" + 138 | "\none = 42 # and even here!" 139 | expect(parser.group).to parse(group_with_comments) 140 | end 141 | it 'can parse assignment irrespective of spacing' do 142 | expect(parser.assignment). 143 | to parse("basic_size_limit= 26214400\n") 144 | expect(parser.assignment). 145 | to parse("basic_size_limit = 26214400\n") 146 | end 147 | it 'can parse single assignment inside a group' do 148 | expect(parser.group). 149 | to parse("[common]\nbasic_size_limit = 26214400\n") 150 | end 151 | it 'can parse multiple assignments inside a group' do 152 | expect(parser.group). 153 | to parse("[common]\nbasic_size_limit = 26214400\nstudent_size_limit = 52428800\n") 154 | end 155 | it 'can parse values including strings' do 156 | group_member = "hostname = #{string}" 157 | expect(parser.group_member).to parse(group_member) 158 | end 159 | it 'can parse multiple groups' do 160 | expect(parser.group).to parse(multi_group) 161 | end 162 | 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/figtree/transformer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Figtree 4 | describe Transformer do 5 | context "can do type conversion" do 6 | let(:int_tree) do 7 | Parser.new.parse("[common]\nbasic_size_limit = 26214400\n") 8 | end 9 | let(:arr_tree) do 10 | Parser.new.parse("[http]\nparams = array,of,values\n") 11 | end 12 | let(:bool_tree) do 13 | Parser.new.parse("[ftp]\nenabled = no\n") 14 | end 15 | let(:string_tree) do 16 | Parser.new.parse_with_debug("[database]\nserver = is here\n") 17 | Parser.new.parse("[database]\nserver = is here\n") 18 | end 19 | let(:ip_tree) do 20 | Parser.new.parse("[database]\nserver = 192.0.2.62") 21 | end 22 | it 'can apply an int type conversion' do 23 | expect(Transformer.new.apply(int_tree)).to eq( 24 | [ 25 | { 26 | common: Subgroup.new({basic_size_limit: 26214400}) 27 | } 28 | ] 29 | ) 30 | end 31 | it 'can apply an array type conversion' do 32 | expect(Transformer.new.apply(arr_tree)).to eq( 33 | [ 34 | { 35 | http: Subgroup.new(params: ["array", "of", "values"]) 36 | } 37 | ] 38 | ) 39 | end 40 | it 'can apply a bool type conversion' do 41 | expect(Transformer.new.apply(bool_tree)).to eq( 42 | [ 43 | { 44 | ftp: Subgroup.new(enabled: false) 45 | } 46 | ] 47 | ) 48 | end 49 | it 'it can apply a string type conversion' do 50 | expect(Transformer.new.apply(string_tree)).to eq( 51 | [ 52 | { 53 | database: Subgroup.new(server: "is here") 54 | } 55 | ] 56 | ) 57 | end 58 | it 'it can apply an ip address type conversion' do 59 | expect(Transformer.new.apply(ip_tree)).to eq( 60 | [ 61 | { 62 | database: Subgroup.new(server: IPAddr.new("192.0.2.62")) 63 | } 64 | ] 65 | ) 66 | end 67 | end 68 | context "overrides by angle brackets" do 69 | let(:override_tree) do 70 | Parser.new.parse("[http]\npath = /srv/\npath = /srv/var/tmp/\n") 71 | end 72 | it 'can apply an override' do 73 | expect(Transformer.new.apply(override_tree, override: :production)).to eq( 74 | [ 75 | { 76 | http: Subgroup.new(path: Pathname.new('/srv/var/tmp/')) 77 | } 78 | ] 79 | ) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/figtree_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Figtree do 4 | describe '#load_config' do 5 | let(:settings_path) { 'spec/support/settings.conf' } 6 | let(:common) do 7 | Figtree::Subgroup.new( 8 | :basic_size_limit => 26214400, 9 | :student_size_limit => 52428800, 10 | :paid_users_size_limit => 2147483648, 11 | :path => Pathname.new("/srv/var/tmp/"), 12 | ) 13 | end 14 | 15 | let(:common_with_override) do 16 | Figtree::Subgroup.new( 17 | :basic_size_limit => 26214400, 18 | :student_size_limit => 52428800, 19 | :paid_users_size_limit => 2147483648, 20 | :path => Pathname.new("/srv/tmp/"), 21 | ) 22 | end 23 | 24 | let(:the_whole_kebab) do 25 | Figtree::IniConfig.new( 26 | [ 27 | { 28 | common: Figtree::Subgroup.new( 29 | { 30 | :basic_size_limit => 26214400, 31 | :student_size_limit=> 52428800, 32 | :paid_users_size_limit=> 2147483648, 33 | :path=> Pathname.new("/srv/var/tmp/") 34 | } 35 | ) 36 | }, 37 | { 38 | ftp: Figtree::Subgroup.new( 39 | { 40 | :name => "\"hello there, ftp uploading\"", 41 | :path => Pathname.new("/tmp/"), 42 | :enabled => false 43 | } 44 | ) 45 | }, 46 | { 47 | http: Figtree::Subgroup.new( 48 | { 49 | :name => "\"http uploading\"", 50 | :path => Pathname.new("/tmp/"), 51 | :params => ["array", "of", "values"] 52 | } 53 | ) 54 | } 55 | ].reduce({}, :merge!) 56 | ) 57 | end 58 | 59 | it 'can parse a group and provide dot notation access' do 60 | expect(Figtree::IniConfig.new(settings_path).common).to eq(common) 61 | end 62 | it 'can parse the overrides correctly' do 63 | expect(Figtree::IniConfig.new(settings_path, :itscript).common). 64 | to eq(common_with_override) 65 | end 66 | it 'can parse the whole Kebab without any misunderstandings' do 67 | expect(Figtree::IniConfig.new(settings_path)).to eq(the_whole_kebab) 68 | end 69 | 70 | it 'can parse the wiki example' do 71 | wiki_example = Figtree::IniConfig.new('spec/support/wiki_example.ini') 72 | expect(wiki_example.database.server).to_not be_nil 73 | end 74 | 75 | context "performance" do 76 | require 'benchmark' 77 | it 'can parse the whole ini file quickly' do 78 | expect( 79 | Benchmark.realtime do 80 | Figtree::IniConfig.new(settings_path) 81 | end 82 | ).to be < 0.025 83 | # without ip_address parsing this was under 0.014 :( 84 | end 85 | end 86 | 87 | context "checking full transform" do 88 | let(:mini_ini_file) { 'spec/support/mini_ini_example.ini' } 89 | let(:mini_ini_config) do 90 | Figtree::IniConfig.new( 91 | { 92 | Network: Figtree::Subgroup.new( 93 | hostname: "My Computer", 94 | address: "dhcp", 95 | dns: IPAddr.new("192.168.1.1") 96 | ) 97 | } 98 | ) 99 | end 100 | 101 | it 'parses unquoted strings properly' do 102 | expect(Figtree::IniConfig.new(mini_ini_file)). 103 | to eq(mini_ini_config) 104 | end 105 | end 106 | 107 | context "comparing to inifile gem" do 108 | it 'does not parse anything INIFile gem cant parse' do 109 | bad_ini_files = Dir["spec/support/invalid/*.ini"] 110 | bad_ini_files.each do |ini_file| 111 | #puts "\nfile is: #{ini_file}" 112 | expect{ Figtree::IniConfig.new("#{ini_file}") }. 113 | to raise_error 114 | end 115 | end 116 | it 'can parse a subset of what INIFile gem can parse' do 117 | ini_files = Dir["spec/support/*.ini"] 118 | ini_files.each do |ini_file| 119 | #puts "\nfile is: #{ini_file}" 120 | expect(Figtree::IniConfig.new("#{ini_file}")). 121 | to be_a Figtree::IniConfig 122 | end 123 | end 124 | end 125 | 126 | context "invalid ini file" do 127 | let(:unparseable_config) do 128 | 'spec/support/invalid/unparseable_settings.conf' 129 | end 130 | let(:untransformable_config) do 131 | 'spec/support/invalid/untransformable_settings.conf' 132 | end 133 | it 'throws ParseFailed if unparseable' do 134 | expect { Figtree::IniConfig.new(unparseable_config) }. 135 | to raise_error(Parslet::ParseFailed) 136 | end 137 | it 'throws TransformFailed if untransformable' do 138 | allow_any_instance_of(String).to receive(:to_b). 139 | and_raise(StandardError) 140 | expect { Figtree::IniConfig.new(untransformable_config) }. 141 | to raise_error(Figtree::TransformFailed) 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | Bundler.setup 3 | 4 | require 'pry' 5 | require 'ostruct' 6 | require 'parslet' 7 | require 'parslet/rig/rspec' 8 | require 'parslet/convenience' 9 | 10 | require 'simplecov' 11 | SimpleCov.start 12 | 13 | Dir[File.join(File.dirname(__FILE__), "..", "lib" , "**.rb")].each do |file| 14 | require file 15 | end 16 | 17 | # because we raise more than one error type from invalid 18 | # this should prob be avoided though 19 | RSpec::Expectations.configuration.warn_about_potential_false_positives = false 20 | 21 | RSpec.configure do |config| 22 | # quiet output from figtree_spec invalid ini context 23 | config.before { allow($stderr).to receive(:puts) } 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/comment.ini: -------------------------------------------------------------------------------- 1 | # comments should be ignored 2 | [section_one] 3 | one = 1 4 | two = 2 5 | 6 | [section_two] # you can comment here 7 | one = 42 # and even here! 8 | multi = a \ # and here, too 9 | b 10 | 11 | -------------------------------------------------------------------------------- /spec/support/good.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 1 3 | two = 2 4 | 5 | [section_two] 6 | three = "-3" 7 | multi = multiline \ 8 | support 9 | 10 | ; comments should be ignored 11 | [section_three] 12 | four =true 13 | five=false # comments can go here 14 | six = "6.0" ; and here, too 15 | 16 | [section_five] 17 | seven_and_eight= "7 & 8" 18 | 19 | -------------------------------------------------------------------------------- /spec/support/invalid/bad_1.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 1 3 | two = 2 4 | 5 | ; the following is not a valid line 6 | invalid line 7 | -------------------------------------------------------------------------------- /spec/support/invalid/bad_2.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 1 3 | two = 2 4 | 5 | [section_two] 6 | close-quote = "some text without 7 | a closing quote - should 8 | be interesting 9 | 10 | [section_three] 11 | wat = where is that closing quote? 12 | -------------------------------------------------------------------------------- /spec/support/invalid/browscap.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/figtree/c5d26e8d917bc71f9454c8751a3dbc5fbcbf44e2/spec/support/invalid/browscap.ini -------------------------------------------------------------------------------- /spec/support/invalid/continuation.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 1 3 | two = 2 4 | 5 | [section_two] 6 | end-of-file = here is the last value \ 7 | -------------------------------------------------------------------------------- /spec/support/invalid/escape.ini: -------------------------------------------------------------------------------- 1 | ; this test file demonstrates escape sequences supported by IniFile 2 | [normal] 3 | foo = http://en.wikipedia.org/wiki/Foobar 4 | 5 | [escaped] 6 | tabs = There is a tab\tcharacter in here somewhere 7 | carriage return = Who uses these anyways?\r 8 | newline = Trust newline!\nAlways there when you need him.\nSplittin' those lines. 9 | null = Who'd be silly enough to put\0 a null character in the middle of a string? \ 10 | Stroustrup would not approve! 11 | backslash = This string \\t contains \\n no \\r special \\0 characters! 12 | quoted = "Escaping works\tinside quoted strings!" 13 | 14 | -------------------------------------------------------------------------------- /spec/support/invalid/global.ini: -------------------------------------------------------------------------------- 1 | ; without any section, a default 'global' section will be created 2 | one = 1 3 | two = 2 4 | -------------------------------------------------------------------------------- /spec/support/invalid/unparseable_settings.conf: -------------------------------------------------------------------------------- 1 | common] 2 | basic_size_limit: 26214400 3 | student_size_limit is 52428800 4 | paid_users_size_limit 2147483648 5 | -------------------------------------------------------------------------------- /spec/support/invalid/untransformable_settings.conf: -------------------------------------------------------------------------------- 1 | [heading] 2 | enabled = no 3 | -------------------------------------------------------------------------------- /spec/support/merge.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 3 3 | 4 | [section_five] 5 | five=5 6 | -------------------------------------------------------------------------------- /spec/support/mini_ini_example.ini: -------------------------------------------------------------------------------- 1 | [Network] 2 | hostname = My Computer 3 | address = dhcp 4 | dns = 192.168.1.1 5 | -------------------------------------------------------------------------------- /spec/support/mixed_comment.ini: -------------------------------------------------------------------------------- 1 | # comments should be ignored 2 | ; multiple comments characters are supported 3 | ; (I'm lookin' at you, samba) 4 | [section_one] 5 | one = 1 6 | two = 2 7 | 8 | -------------------------------------------------------------------------------- /spec/support/multiline.ini: -------------------------------------------------------------------------------- 1 | [section_one] 2 | one = 1 3 | two = 2 4 | 5 | [section_two] 6 | three = 3 7 | 8 | ; comments should be ignored 9 | [section_three] 10 | three = hello \ 11 | multiline 12 | other = "stuff" 13 | 14 | [section_four] 15 | four = hello \ # comments work here, too 16 | multiple \ # and here !!! 17 | multilines # and even here (OMG) 18 | five = "multiple lines 19 | inside of quotations 20 | preserve everything" 21 | 22 | [empty_lines] 23 | not_empty=full 24 | -------------------------------------------------------------------------------- /spec/support/multiline_only.ini: -------------------------------------------------------------------------------- 1 | [section_three] 2 | two = 2 3 | three = hello \ 4 | multiline 5 | other = "stuff" 6 | -------------------------------------------------------------------------------- /spec/support/param.ini: -------------------------------------------------------------------------------- 1 | ; comments should be ignored 2 | [section_one] 3 | one = 1 4 | two=2 5 | 6 | -------------------------------------------------------------------------------- /spec/support/section.ini: -------------------------------------------------------------------------------- 1 | ; section headers can be values 2 | [section_one] 3 | one=[value] 4 | two=2 5 | -------------------------------------------------------------------------------- /spec/support/settings.conf: -------------------------------------------------------------------------------- 1 | [common] 2 | basic_size_limit = 26214400 3 | student_size_limit = 52428800 4 | paid_users_size_limit = 2147483648 5 | path = /srv/var/tmp/ 6 | path = /srv/tmp/ 7 | [ftp] 8 | name = "hello there, ftp uploading" 9 | path = /tmp/ 10 | path = /srv/var/tmp/ 11 | path = /srv/uploads/ 12 | path = /etc/var/uploads 13 | enabled = no 14 | ; This is a comment 15 | [http] 16 | name = "http uploading" 17 | path = /tmp/ 18 | path = /srv/var/tmp/ 19 | path = /srv/uploads/; This is another comment 20 | params = array,of,values 21 | 22 | -------------------------------------------------------------------------------- /spec/support/wiki_example.ini: -------------------------------------------------------------------------------- 1 | ; last modified 1 April 2001 by John Doe 2 | [owner] 3 | name="John Doe" 4 | organization="Acme Widgets Inc." 5 | 6 | [database] 7 | ; use IP address in case network name resolution is not working 8 | server=192.0.2.62 9 | port=143 10 | file="payroll.dat" 11 | --------------------------------------------------------------------------------