├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── key_path.gemspec ├── lib ├── key_path.rb └── key_path │ ├── enumerable │ └── extensions.rb │ ├── hash │ ├── deep_merge.rb │ └── extensions.rb │ ├── path.rb │ ├── string │ └── extensions.rb │ └── version.rb └── spec ├── enumerable_extensions_spec.rb ├── hash_deep_merge_spec.rb ├── hash_extensions_spec.rb ├── key_path_spec.rb ├── path_spec.rb ├── spec_helper.rb └── string_extensions_spec.rb /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [2.5, 2.6, 2.7, '3.0', 3.1, 3.2] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Ruby ${{ matrix.ruby }} 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: ${{ matrix.ruby }} 18 | - name: Update system gem for Ruby 2.5, 2.6 19 | run: gem update --system 3.2.3 20 | if: ${{ matrix.ruby <= '2.6' }} 21 | - name: Install dependencies 22 | run: bundle install 23 | - name: Run tests 24 | run: bundle exec rake 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.2.0 4 | 5 | * Bumps the development version of Ruby to 2.3.0. 6 | * Allows for activesupport 5.0 (@majioa). 7 | 8 | ## 1.1.0 9 | 10 | * (5a95e95) Removes the usage of `eval`. (@PikachuEXE) 11 | * (625e295) Switches to Travis container infrastructure. (@PikachuEXE) 12 | * Switches to looser Ruby declarations to improve the coverage. 13 | * Adds tests against Ruby HEAD (but can fail). 14 | 15 | ## 1.0.1 16 | 17 | * (e618da5) Adds a test case for strings with capital letters. (@samuel-hcl) 18 | * (5601a9f) Fixes an exception when setting a string with capital letters. (@samuel-hcl) 19 | * (386a925) Updates the README to mention the gem correctly. 20 | * (64f3649) Improves the installation instructions. 21 | * (fdcf7d5) Adds CodeClimate for quality and coverage testing. 22 | * (968b30f) Tests against Ruby 2.1.5, 2.2.0. 23 | * (47a2663) Fixes Rubocop offenses. 24 | 25 | ## 1.0.0 26 | 27 | * (460ff50) Initial release; implements the basic functionality for handling 28 | "key paths" in Array and Hash objects. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in keypath.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Nick Charlton 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keypath-based collection access extensions for Ruby. 2 | 3 | [![Tests](https://github.com/nickcharlton/keypath-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/nickcharlton/keypath-ruby/actions/workflows/tests.yml) 4 | 5 | This gem allows you to access nested Ruby collections (`Hash`, `Array`, etc) using 6 | keypaths. 7 | 8 | For example, say you had a nested data structure like: 9 | 10 | ```ruby 11 | data = { 12 | :item_one => {:id => 1, :url => 'http://nickcharlton.net/'}, 13 | :something_else => [ 14 | { 15 | :id => 1, 16 | :url => 'https://github.com/' 17 | } 18 | ] 19 | } 20 | ``` 21 | 22 | You could access "https://github.com/" through: `something_else.0.url`. Basically, 23 | this is intended to allow you to manipulate/transform large nested structures that 24 | you might get back from a JSON document. 25 | 26 | ## Installation 27 | 28 | `keypath-ruby` is on [RubyGems][] as `key_path`. But you can also add the repo 29 | to your [Gemfile][]: 30 | 31 | ```ruby 32 | gem 'key_path', :git => 'https://github.com/nickcharlton/keypath-ruby.git' 33 | ``` 34 | 35 | ## Usage 36 | 37 | `KeyPath` is at least two things. First, it's a class (actually, `KeyPath::Path`) 38 | which represents a path (this is just a string, and has methods to go back and 39 | forth from it) and secondly a set of class extensions for `Enumerable`, `Hash` and 40 | `String` which allow you to use the native collection classes with keypaths. 41 | 42 | ```ruby 43 | require 'key_path' 44 | 45 | data = { 46 | :item => { 47 | :url => 'http://nickcharlton.net' 48 | } 49 | } 50 | 51 | # fetching a path 52 | path = KeyPath::Path.new 'item.url' 53 | data.value_at_keypath(path) #=> 'http://nickcharlton.net' 54 | 55 | # finding all `:url` paths in a collection 56 | data.keypaths_for_nested_key(:url) #=> {item.url => 'http://nickcharlton.net'} 57 | 58 | # going back and forth from a string 59 | path.to_s #=> 'item.url' 60 | 'item.url'.to_keypath #=> # 61 | 62 | # get the parent of a keypath (or nil, if there isn't one) 63 | path.parent #=> # 64 | 65 | # setting a path 66 | data.set_keypath(path, 'http://github.com/') 67 | ``` 68 | 69 | ## Contributing 70 | 71 | 1. Fork it 72 | 2. Create your feature branch (`git checkout -b my-new-feature`) 73 | 3. Commit your changes (`git commit -am 'Add some feature'`) 74 | 4. Push to the branch (`git push origin my-new-feature`) 75 | 5. Create new Pull Request 76 | 77 | ## Author 78 | 79 | Copyright (c) 2013 Nick Charlton () 80 | 81 | [RubyGems]: http://rubygems.org/ 82 | [Gemfile]: http://bundler.io/ 83 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | ## 2 | # Initialise Bundler, catch errors. 3 | ## 4 | require 'bundler' 5 | require 'bundler/gem_tasks' 6 | 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts 'Run `bundle install` to install missing gems.' 12 | exit e.status_code 13 | end 14 | 15 | ## 16 | # Configure the test suite. 17 | ## 18 | require 'rake/testtask' 19 | 20 | Rake::TestTask.new :spec do |t| 21 | t.test_files = Dir['spec/*_spec.rb'] 22 | end 23 | 24 | ## 25 | # By default, just run the tests. 26 | ## 27 | task default: :spec 28 | -------------------------------------------------------------------------------- /key_path.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'key_path/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'key_path' 8 | spec.version = KeyPath::VERSION 9 | spec.authors = ['Nick Charlton'] 10 | spec.email = ['nick@nickcharlton.net'] 11 | spec.description = 'Keypath-based collection access extensions for Ruby.' 12 | spec.summary = 'This gem allows you to access nested Ruby' \ 13 | 'collections (Hash, Array, etc) using keypaths.' \ 14 | "e.g.: 'something.item.0'" 15 | spec.homepage = 'https://github.com/nickcharlton/keypath-ruby' 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files`.split($RS) 19 | spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(/^(test|spec|features)/) 21 | spec.require_paths = ['lib'] 22 | 23 | spec.add_dependency 'activesupport', '>= 4.0' 24 | 25 | spec.add_development_dependency 'bundler' 26 | spec.add_development_dependency 'rake' 27 | spec.add_development_dependency 'pry' 28 | end 29 | -------------------------------------------------------------------------------- /lib/key_path.rb: -------------------------------------------------------------------------------- 1 | # gem bits 2 | require 'key_path/version' 3 | require 'key_path/path' 4 | 5 | # extensions 6 | require 'key_path/hash/deep_merge' 7 | require 'key_path/hash/extensions' 8 | require 'key_path/enumerable/extensions' 9 | require 'key_path/string/extensions' 10 | 11 | module KeyPath 12 | end 13 | -------------------------------------------------------------------------------- /lib/key_path/enumerable/extensions.rb: -------------------------------------------------------------------------------- 1 | module Enumerable 2 | # see: http://stackoverflow.com/a/7139631/83386 3 | def value_at_keypath(keypath) 4 | keypath = keypath.to_s if keypath.is_a?(KeyPath::Path) 5 | 6 | parts = keypath.split '.', 2 7 | 8 | # if it's an array, call the index 9 | if self[parts[0].to_i] 10 | match = self[parts[0].to_i] 11 | else 12 | match = self[parts[0]] || self[parts[0].to_sym] 13 | end 14 | 15 | if !parts[1] || match.nil? 16 | return match 17 | else 18 | return match.value_at_keypath(parts[1]) 19 | end 20 | end 21 | 22 | def set_keypath(keypath, value) 23 | # handle both string and KeyPath::Path forms 24 | keypath = keypath.to_keypath if keypath.is_a?(String) 25 | 26 | keypath_parts = keypath.to_a 27 | # Return self if path empty 28 | return self if keypath_parts.empty? 29 | 30 | key = keypath_parts.shift 31 | # Just assign value to self when it's a direct path 32 | # Remember, this is after calling keypath_parts#shift 33 | if keypath_parts.length == 0 34 | key = key.is_number? ? Integer(key) : key.to_sym 35 | 36 | self[key] = value 37 | return self 38 | end 39 | 40 | # keypath_parts.length > 0 41 | # Remember, this is after calling keypath_parts#shift 42 | collection = if key.is_number? 43 | Array.new 44 | else 45 | Hash.new 46 | end 47 | 48 | # Remember, this is after calling keypath_parts#shift 49 | collection.set_keypath(keypath_parts.join('.'), value) 50 | 51 | # merge the new collection into self 52 | self[key] = collection 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/key_path/hash/deep_merge.rb: -------------------------------------------------------------------------------- 1 | module KeyPath 2 | # 3 | # = Hash Deep Merge 4 | # 5 | # Merges a Ruby Hash recursively, Also known as deep merge. 6 | # Recursive version of Hash#merge and Hash#merge!. 7 | # 8 | # Category:: Ruby 9 | # Package:: Hash 10 | # Author:: Simone Carletti 11 | # Copyright:: 2007-2008 The Authors 12 | # License:: MIT License 13 | # Link:: http://www.simonecarletti.com/ 14 | # Source:: https://gist.github.com/weppos/6391 15 | # 16 | module HashDeepMerge 17 | # 18 | # Recursive version of Hash#merge! 19 | # 20 | # Adds the contents of +other_hash+ to +hsh+, 21 | # merging entries in +hsh+ with duplicate keys with those from +other_hash+. 22 | # 23 | # Compared with Hash#merge!, this method supports nested hashes. 24 | # When both +hsh+ and +other_hash+ contains an entry with the same key, 25 | # it merges and returns the values from both arrays. 26 | # 27 | # h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}} 28 | # h2 = {"b" => 254, "c" => 300, "c" => {"c1" => 16, "c3" => 94}} 29 | # h1.rmerge!(h2) #=> {"a" => 100, "b" => 254, 30 | # "c" => {"c1" => 16, "c2" => 14, "c3" => 94}} 31 | # 32 | # Simply using Hash#merge! would return 33 | # 34 | # h1.merge!(h2) #=> {"a" => 100, "b" = >254, 35 | # "c" => {"c1" => 16, "c3" => 94}} 36 | # 37 | def deep_merge!(other_hash) 38 | merge!(other_hash) do |_key, oldval, newval| 39 | oldval.class == self.class ? oldval.deep_merge!(newval) : newval 40 | end 41 | end 42 | 43 | # 44 | # Recursive version of Hash#merge 45 | # 46 | # Compared with Hash#merge!, this method supports nested hashes. 47 | # When both +hsh+ and +other_hash+ contains an entry with the same key, 48 | # it merges and returns the values from both arrays. 49 | # 50 | # Compared with Hash#merge, this method provides a different approch 51 | # for merging nasted hashes. 52 | # If the value of a given key is an Hash and both +other_hash+ abd +hsh 53 | # includes the same key, the value is merged instead replaced with 54 | # +other_hash+ value. 55 | # 56 | # h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}} 57 | # h2 = {"b" => 254, "c" => 300, "c" => {"c1" => 16, "c3" => 94}} 58 | # h1.rmerge(h2) #=> {"a" => 100, "b" => 254, 59 | # "c" => {"c1" => 16, "c2" => 14, "c3" => 94}} 60 | # 61 | # Simply using Hash#merge would return 62 | # 63 | # h1.merge(h2) #=> {"a" => 100, "b" = >254, 64 | # "c" => {"c1" => 16, "c3" => 94}} 65 | # 66 | def deep_merge(other_hash) 67 | r = {} 68 | merge(other_hash) do |key, oldval, newval| 69 | r[key] = oldval.class == self.class ? oldval.deep_merge(newval) : newval 70 | end 71 | end 72 | end 73 | end 74 | 75 | # Mixin HashDeepMerge into the standard Hash. 76 | class Hash 77 | include KeyPath::HashDeepMerge 78 | end 79 | -------------------------------------------------------------------------------- /lib/key_path/hash/extensions.rb: -------------------------------------------------------------------------------- 1 | module KeyPath 2 | # A Mixin for Hash which allows us to create a path from a key. 3 | module HashExtensions 4 | def keypaths_for_nested_key(nested_key = '', nested_hash = self, 5 | path = [], all_values = {}) 6 | nested_hash.each do |k, v| 7 | path << k.to_s # assemble the path from the key 8 | case v 9 | when Array then 10 | v.each_with_index do |item, i| 11 | path << "#{i}" # add the array key 12 | keypaths_for_nested_key(nested_key, item, path, all_values) 13 | end 14 | path.pop # remove the array key 15 | when Hash then keypaths_for_nested_key(nested_key, v, path, all_values) 16 | else 17 | all_values.merge!("#{path.join('.')}" => "#{v}") if k == nested_key 18 | 19 | path.pop 20 | end 21 | end 22 | path.pop 23 | 24 | all_values 25 | end 26 | end 27 | end 28 | 29 | # Mix `HashExtensions` into `Hash`. 30 | class Hash 31 | include KeyPath::HashExtensions 32 | end 33 | -------------------------------------------------------------------------------- /lib/key_path/path.rb: -------------------------------------------------------------------------------- 1 | module KeyPath 2 | # The class representing a Path in a collection object. 3 | class Path 4 | def initialize(path = '') 5 | @path = path 6 | end 7 | 8 | def parent 9 | s = to_a 10 | s.pop 11 | 12 | # there's no parent if it's empty 13 | return nil if s == [] 14 | 15 | # otherwise, join them back together and pass back a path 16 | s.join('.').to_keypath 17 | end 18 | 19 | def to_s 20 | @path 21 | end 22 | 23 | def to_a 24 | @path.split('.') 25 | end 26 | 27 | def inspect 28 | "#<#{self.class.name}:#{object_id} path=#{@path}>" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/key_path/string/extensions.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module KeyPath 4 | # Extensions to `String`. 5 | module StringExtensions 6 | def to_keypath 7 | KeyPath::Path.new self 8 | end 9 | 10 | def is_singular? 11 | pluralize != self && singularize == self 12 | end 13 | 14 | def is_plural? 15 | singularize != self && pluralize == self 16 | end 17 | 18 | def is_number? 19 | true if Float(self) rescue false 20 | end 21 | end 22 | end 23 | 24 | # Mix `StringExtensions` into `String`. 25 | class String 26 | include KeyPath::StringExtensions 27 | end 28 | -------------------------------------------------------------------------------- /lib/key_path/version.rb: -------------------------------------------------------------------------------- 1 | # Version declaration. 2 | module KeyPath 3 | VERSION = '1.2.0' 4 | end 5 | -------------------------------------------------------------------------------- /spec/enumerable_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | 4 | describe 'EnumerableExtensions' do 5 | it 'adds methods to collections' do 6 | hash = { id: 1 } 7 | 8 | hash.must_respond_to 'value_at_keypath' 9 | hash.must_respond_to 'set_keypath' 10 | end 11 | 12 | it 'can fetch simple path values' do 13 | hash = { id: 1 } 14 | 15 | hash.value_at_keypath('id').must_equal 1 16 | end 17 | 18 | it 'can accept KeyPath::Path objects for keypaths' do 19 | hash = { id: 1 } 20 | keypath = KeyPath::Path.new('id') 21 | 22 | hash.value_at_keypath(keypath).must_equal 1 23 | end 24 | 25 | it 'can fetch with a nested hash key path' do 26 | hash = { item: { id: 1 } } 27 | 28 | hash.value_at_keypath('item.id').must_equal 1 29 | end 30 | 31 | it 'can fetch a nested array object' do 32 | hash = { items: [{ id: 1 }] } 33 | 34 | hash.value_at_keypath('items.0').must_equal(id: 1) 35 | end 36 | 37 | it 'can set a simple value using a keypath string' do 38 | source = { item: { id: {} } } 39 | keypath = 'item.id' 40 | value = 1 41 | 42 | source.set_keypath(keypath, value) 43 | 44 | source.value_at_keypath(keypath).must_equal(value) 45 | end 46 | 47 | it 'can set a simple value using a KeyPath::Path object' do 48 | source = { item: { id: {} } } 49 | keypath = KeyPath::Path.new('item.id') 50 | value = 1 51 | 52 | source.set_keypath(keypath, value) 53 | 54 | source.value_at_keypath(keypath).must_equal(value) 55 | end 56 | 57 | it 'can set a string value using a KeyPath::Path object' do 58 | source = { item: { id: {} } } 59 | keypath = KeyPath::Path.new('item.id') 60 | value = 'value' 61 | 62 | source.set_keypath(keypath, value) 63 | 64 | source.value_at_keypath(keypath).must_equal(value) 65 | end 66 | 67 | it 'can set a string with capital letters' do 68 | source = { item: { id: {} } } 69 | keypath = KeyPath::Path.new('item.id') 70 | value = 'VALUE' 71 | 72 | source.set_keypath(keypath, value) 73 | 74 | source.value_at_keypath(keypath).must_equal(value) 75 | end 76 | 77 | it 'can set a hash for a path' do 78 | source = { item: { id: {} } } 79 | keypath = KeyPath::Path.new('item') 80 | value = { id: 1 } 81 | 82 | source.set_keypath(keypath, value) 83 | 84 | source.value_at_keypath(keypath).must_equal(value) 85 | end 86 | 87 | it 'can set a value in a nested array' do 88 | source = { items: [{ id: 1 }] } 89 | keypath = KeyPath::Path.new('items.1') 90 | value = { id: 2 } 91 | 92 | source.set_keypath(keypath, value) 93 | 94 | source.value_at_keypath(keypath).must_equal(value) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/hash_deep_merge_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | 4 | describe 'HashDeepMerge' do 5 | it 'adds methods to the Hash class' do 6 | hash = { id: 1 } 7 | 8 | hash.must_respond_to 'deep_merge' 9 | hash.must_respond_to 'deep_merge!' 10 | end 11 | 12 | it 'recursively adds two hashes' do 13 | one = { one: { id: 1 } } 14 | two = { one: { url: 'https://nickcharlton.net' } } 15 | three = { one: { id: 1, url: 'https://nickcharlton.net' } } 16 | 17 | output = one.deep_merge(two) 18 | 19 | output.must_be_kind_of Hash 20 | output.must_equal three 21 | end 22 | 23 | it 'recursively adds two hashes in place' do 24 | one = { one: { id: 1 } } 25 | two = { one: { url: 'https://nickcharlton.net' } } 26 | three = { one: { id: 1, url: 'https://nickcharlton.net' } } 27 | 28 | one.deep_merge!(two) 29 | 30 | one.must_be_kind_of Hash 31 | one.must_equal three 32 | end 33 | end 34 | 35 | describe 'HashNormalMerge' do 36 | it 'combines nested hashes without decending into them' do 37 | one = { 'a' => 100, 'b' => 200, 'c' => { 'c1' => 12, 'c2' => 14 } } 38 | two = { 'b' => 254, 'c' => 300, 'c' => { 'c1' => 16, 'c3' => 94 } } 39 | expected = { 'a' => 100, 'b' => 254, 'c' => { 'c1' => 16, 'c3' => 94 } } 40 | 41 | output = one.merge(two) 42 | 43 | output.must_be_kind_of Hash 44 | output.must_equal expected 45 | end 46 | 47 | it 'combines nested hashes without decending into them in place' do 48 | one = { 'a' => 100, 'b' => 200, 'c' => { 'c1' => 12, 'c2' => 14 } } 49 | two = { 'b' => 254, 'c' => 300, 'c' => { 'c1' => 16, 'c3' => 94 } } 50 | expected = { 'a' => 100, 'b' => 254, 'c' => { 'c1' => 16, 'c3' => 94 } } 51 | 52 | one.merge!(two) 53 | 54 | one.must_be_kind_of Hash 55 | one.must_equal expected 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/hash_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | 4 | describe 'HashExtensions' do 5 | it 'adds methods to the Hash class' do 6 | hash = { id: 1 } 7 | 8 | hash.must_respond_to 'keypaths_for_nested_key' 9 | end 10 | 11 | it 'can find keys in a nested hash' do 12 | data = { 13 | id: 1, 14 | item: { 15 | id: 2, 16 | name: 'an item' 17 | } 18 | } 19 | 20 | keypaths = data.keypaths_for_nested_key(:id) 21 | 22 | keypaths.wont_be_nil 23 | keypaths.must_include 'id' 24 | keypaths.must_include 'item.id' 25 | end 26 | 27 | it 'can find keys in nested array' do 28 | data = { 29 | id: 1, 30 | items: [{ 31 | id: 2, 32 | name: 'an item' 33 | }, { 34 | id: 3, 35 | name: 'another item' 36 | }] 37 | } 38 | 39 | keypaths = data.keypaths_for_nested_key(:id) 40 | 41 | keypaths.wont_be_nil 42 | keypaths.must_include 'id' 43 | keypaths.must_include 'items.0.id' 44 | keypaths.must_include 'items.1.id' 45 | end 46 | 47 | it 'can handle walking into an otherwise unknown object' do 48 | # Example class to demonstrate an unknown object. 49 | class ExampleClass 50 | def initialize 51 | @name = 'Example Class' 52 | end 53 | 54 | example = ExampleClass.new 55 | 56 | data = { items: example } 57 | 58 | keypaths = data.keypaths_for_nested_key(:items) 59 | 60 | keypaths.wont_be_nil 61 | keypaths.must_include 'items' 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/key_path_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | -------------------------------------------------------------------------------- /spec/path_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | 4 | describe 'KeyPath::Path main methods' do 5 | it 'creates a KeyPath instance from a string' do 6 | path = KeyPath::Path.new('item.url') 7 | 8 | path.wont_be_nil 9 | path.to_s.must_be_kind_of String 10 | path.to_s.must_equal 'item.url' 11 | end 12 | 13 | it 'outputs an array of items in the path' do 14 | path = KeyPath::Path.new('item.url') 15 | 16 | path.to_a.must_be_kind_of Array 17 | path.to_a.must_equal %w(item url) 18 | end 19 | 20 | it 'spits out a useful inspect string' do 21 | path = KeyPath::Path.new('item.url') 22 | 23 | path.inspect.must_be_kind_of String 24 | path.inspect.wont_be_nil 25 | end 26 | 27 | it 'returns a parent if one exists' do 28 | path = KeyPath::Path.new('item.url') 29 | 30 | path.parent.wont_be_nil 31 | path.parent.must_be_kind_of KeyPath::Path 32 | end 33 | 34 | it 'returns nil if a parent doesnt exist' do 35 | path = KeyPath::Path.new('item') 36 | 37 | path.parent.must_be_nil 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # test framework 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | 5 | # pull in the library 6 | require File.expand_path '../lib/key_path.rb', __dir__ 7 | -------------------------------------------------------------------------------- /spec/string_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # test helpers 2 | require File.expand_path 'spec_helper.rb', __dir__ 3 | 4 | describe 'StringExtensions' do 5 | it 'can create a KeyPath instance from itself' do 6 | example_string = 'item.url' 7 | 8 | example_string.to_keypath.wont_be_nil 9 | example_string.to_keypath.must_be_kind_of KeyPath::Path 10 | 11 | # around and around we go 12 | example_string.to_keypath.to_s.must_equal example_string 13 | end 14 | 15 | it 'can make a set of strings plural' do 16 | %w(word rail dress business).each do |v| 17 | v.is_plural?.must_equal false 18 | end 19 | 20 | %w(words rails dresses businesses).each do |v| 21 | v.is_plural?.must_equal true 22 | end 23 | end 24 | 25 | it 'can make a set of string singular' do 26 | %w(word rail dress business).each do |v| 27 | v.is_singular?.must_equal true 28 | end 29 | 30 | %w(words rails dresses businesses).each do |v| 31 | v.is_singular?.must_equal false 32 | end 33 | end 34 | 35 | it 'can test if a string is actually a number' do 36 | '0'.is_number?.must_equal true 37 | '1234567890'.is_number?.must_equal true 38 | 39 | 'item'.is_number?.must_equal false 40 | end 41 | end 42 | --------------------------------------------------------------------------------