├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── dig_rb.gemspec ├── lib ├── dig_rb.rb └── dig_rb │ ├── array.rb │ ├── hash.rb │ ├── ostruct.rb │ ├── struct.rb │ └── version.rb └── test ├── minitest_helper.rb ├── ruby_spec ├── array-dig_spec.rb └── hash-dig_spec.rb ├── test_230.rb └── test_dig_rb.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.4 4 | - 2.1.8 5 | - 2.0.0-p648 6 | - jruby-9.0.4.0 7 | - jruby-1.7.23 8 | - 2.3.0 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in dig_rb.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jonathan Rochkind 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 | # Dig_rb 2 | 3 | [![Gem Version](https://badge.fury.io/rb/dig_rb.svg)](https://badge.fury.io/rb/dig_rb) [![Build Status](https://travis-ci.org/jrochkind/dig_rb.svg?branch=master)](https://travis-ci.org/jrochkind/dig_rb) 4 | 5 | [Ruby 2.3.0 introduced #dig on Hash, Array, and Struct](https://www.ruby-lang.org/en/news/2015/12/25/ruby-2-3-0-released/). With this gem, you can have dig on ruby pre 2.3.0, or any ruby lacking dig. 6 | 7 | If you are writing an app and want to use dig in it you should probably just upgrade to ruby 2.3.0. But if you are writing a gem and want it to work with both MRI 2.3.0 and others (including JRuby 9.0.x), this gem is for you. This gem only adds #dig methods if they aren't already defined, so it's safe to use in code that is for all rubies, if run on MRI 2.3.0 you'll still be using native #dig, otherwise dig_rb's implementation. 8 | 9 | ### Will it work identically to MRI 2.3.0 dig? 10 | 11 | Dig_rb is tested with: 12 | 13 | * Specs found in MRI repo for #dig in 2.3.0 14 | * [Ruby Spec Suite](https://github.com/ruby/spec/) specs found in repo for Array and Hash#dig 15 | * All examples in MRI 2.3.0 generated method API docs. (One example in MRI 2.3.0 is _wrong_ about exception class and message returned, dig_rb matches actual 2.3.0 behavior there, not documented example) 16 | 17 | [Our travis](https://travis-ci.org/jrochkind/dig_rb) runs tests on a variety of ruby platforms, including 2.3.0 itself to make sure our tested behavior is what built-in dig in 2.3.0 does too. 18 | 19 | If you find any weird edge cases that work differenty in MRI 2.3.0 than in ruby_dig, let me know in a GitHub Issue please. 20 | 21 | The performance of dig_rb will probably be less than native MRI 2.3.0 implementation, this code is not written for performance. But it should 22 | be fine, really. 23 | 24 | ## Installation 25 | 26 | Add this line to your application's Gemfile: 27 | 28 | ```ruby 29 | gem 'dig_rb' 30 | ``` 31 | 32 | And then execute: 33 | 34 | $ bundle 35 | 36 | Or install it yourself as: 37 | 38 | $ gem install dig_rb 39 | 40 | ## Usage 41 | 42 | Just go ahead and use #dig as doc'd in MRI 2.3.0, now it'll work on any ruby. 43 | 44 | * [Hash](http://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig) 45 | * [Array](http://ruby-doc.org/core-2.3.0/Array.html#method-i-dig) 46 | * [Struct](http://ruby-doc.org/core-2.3.0/Struct.html#method-i-dig) 47 | * [OpenStruct](http://ruby-doc.org/stdlib-2.3.0/libdoc/ostruct/rdoc/OpenStruct.html#method-i-dig) 48 | 49 | ## Development 50 | 51 | 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. 52 | 53 | 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). 54 | 55 | ## Contributing 56 | 57 | 1. Fork it ( https://github.com/jrochkind/dig_rb/fork ) 58 | 2. Create your feature branch (`git checkout -b my-new-feature`) 59 | 3. Commit your changes (`git commit -am 'Add some feature'`) 60 | 4. Push to the branch (`git push origin my-new-feature`) 61 | 5. Create a new Pull Request 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | t.test_files = FileList['test/**/test*.rb'] + FileList['test/**/*_spec.rb'] 7 | t.verbose = true 8 | end 9 | 10 | task :default => [:test] -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "dig_rb" 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 "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dig_rb.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dig_rb/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "dig_rb" 8 | spec.version = DigRb::VERSION 9 | spec.authors = ["Jonathan Rochkind"] 10 | spec.email = ["jonathan@dnil.net"] 11 | 12 | spec.summary = %q{Array/Hash/Struct#dig backfill for ruby} 13 | spec.homepage = "https://github.com/jrochkind/dig_rb" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | #spec.add_development_dependency "bundler", "~> 1.9" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | spec.add_development_dependency "minitest", "~> 5.8.0" 24 | end 25 | -------------------------------------------------------------------------------- /lib/dig_rb.rb: -------------------------------------------------------------------------------- 1 | require 'dig_rb/version' 2 | require 'dig_rb/hash' 3 | require 'dig_rb/array' 4 | require 'dig_rb/struct' 5 | require 'dig_rb/ostruct' 6 | 7 | module DigRb 8 | def self.guard_dig(obj) 9 | unless obj.respond_to?(:dig) 10 | raise TypeError, "#{obj.class.name} does not have #dig method" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/dig_rb/array.rb: -------------------------------------------------------------------------------- 1 | unless Array.instance_methods.include?(:dig) 2 | Array.class_eval do 3 | def dig(key, *args) 4 | value = self.at(key) 5 | return value if args.length == 0 || value.nil? 6 | DigRb.guard_dig(value) 7 | value.dig(*args) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/dig_rb/hash.rb: -------------------------------------------------------------------------------- 1 | unless Hash.instance_methods.include?(:dig) 2 | Hash.class_eval do 3 | # Retrieves the value object corresponding to the each key objects repeatedly. 4 | # 5 | # h = { foo: {bar: {baz: 1}}} 6 | # h.dig(:foo, :bar, :baz) #=> 1 7 | # h.dig(:foo, :zot) #=> nil 8 | def dig(key, *args) 9 | value = self[key] 10 | return value if args.length == 0 || value.nil? 11 | DigRb.guard_dig(value) 12 | value.dig(*args) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/dig_rb/ostruct.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | unless OpenStruct.instance_methods.include?(:dig) 3 | OpenStruct.class_eval do 4 | # 5 | # Retrieves the value object corresponding to the each +name+ 6 | # objects repeatedly. 7 | # 8 | # address = OpenStruct.new('city' => "Anytown NC", 'zip' => 12345) 9 | # person = OpenStruct.new('name' => 'John Smith', 'address' => address) 10 | # person.dig(:address, 'zip') # => 12345 11 | # person.dig(:business_address, 'zip') # => nil 12 | # 13 | def dig(name, *args) 14 | begin 15 | name = name.to_sym 16 | rescue NoMethodError 17 | raise TypeError, "#{name} is not a symbol nor a string" 18 | end 19 | return nil unless self.respond_to?(name) 20 | value = self.send(name) 21 | return value if args.length == 0 || value.nil? 22 | DigRb.guard_dig(value) 23 | value.dig(*args) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/dig_rb/struct.rb: -------------------------------------------------------------------------------- 1 | unless Struct.instance_methods.include?(:dig) 2 | Struct.class_eval do 3 | 4 | # Extracts the nested value specified by the sequence of idx 5 | # objects by calling +dig+ at each step, returning +nil+ if any 6 | # intermediate step is +nil+. 7 | 8 | # klass = Struct.new(:a) 9 | # o = klass.new(klass.new({b: [1, 2, 3]})) 10 | 11 | # o.dig(:a, :a, :b, 0) #=> 1 12 | # o.dig(:b, 0) #=> nil 13 | def dig(key, *args) 14 | value = if key.respond_to?(:to_sym) 15 | return nil unless self.respond_to?(key.to_sym) 16 | self.send(key.to_sym) 17 | elsif key.respond_to?(:to_int) 18 | return nil unless self.length >= key.to_int + 1 19 | self[key.to_int] 20 | else 21 | raise TypeError, "no implicit conversion of #{key.class} into Integer" 22 | end 23 | 24 | return value if args.length == 0 || value.nil? 25 | DigRb.guard_dig(value) 26 | value.dig(*args) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dig_rb/version.rb: -------------------------------------------------------------------------------- 1 | module DigRb 2 | VERSION = "1.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'dig_rb' 4 | 5 | require 'minitest/autorun' 6 | require 'minitest/spec' 7 | 8 | def ruby_version_at_least_2_3_0? 9 | Gem::Dependency.new('', '>= 2.3.0').match?('', RUBY_VERSION) 10 | end 11 | 12 | if ruby_version_at_least_2_3_0? 13 | $stderr.puts "\nDig_rb: Running tests under ruby version #{RUBY_VERSION} (>= 2.3.0), we will test to make sure we are NOT patching #dig, and run other tests to ensure native ruby #dig in this ruby version meets the same specs as our patched version in other ruby versions.\n\n" 14 | end -------------------------------------------------------------------------------- /test/ruby_spec/array-dig_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | # https://github.com/ruby/spec/blob/f8358bd32e6d2c492f8d7e7bb5a35524d2756c3c/core/array/dig_spec.rb 4 | describe "Array#dig" do 5 | it "returns #at with one arg" do 6 | assert_equal 'a', ['a'].dig(0) 7 | assert_nil ['a'].dig(1) 8 | end 9 | 10 | it "recurses array elements" do 11 | a = [ [ 1, [2, '3'] ] ] 12 | assert_equal 1, a.dig(0, 0) 13 | assert_equal '3', a.dig(0, 1, 1) 14 | assert_equal 2, a.dig(0, -1, 0) 15 | end 16 | 17 | it "raises without any args" do 18 | e = assert_raises(ArgumentError) { [10].dig() } 19 | # jrochkind added... 20 | assert_match /\Awrong number of arguments/, e.message 21 | end 22 | 23 | it "calls #dig on the result of #at with the remaining arguments" do 24 | h = [[nil, [nil, nil, 42]]] 25 | 26 | # We don't have the test infrastructure for should_receive 27 | #h[0].should_receive(:dig).with(1, 2).and_return(42) 28 | 29 | assert_equal 42, h.dig(0, 1, 2) 30 | end 31 | end -------------------------------------------------------------------------------- /test/ruby_spec/hash-dig_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | # https://github.com/ruby/spec/blob/9011723c1bb4346bd9c2a6b8a88195d29b5c6e41/core/hash/dig_spec.rb 4 | describe "Hash#dig" do 5 | it "returns #[] with one arg" do 6 | h = { 0 => false, a: 1 } 7 | assert_equal(1, h.dig(:a)) 8 | assert_equal(false, h.dig(0)) 9 | assert_nil(h.dig(1)) 10 | end 11 | 12 | it "does recurse" do 13 | h = { foo: { bar: { baz: 1 } } } 14 | assert_equal(1, h.dig(:foo, :bar, :baz)) 15 | assert_nil h.dig(:foo, :bar, :nope) 16 | assert_nil h.dig(:foo, :baz) 17 | assert_nil h.dig(:bar, :baz, :foo) 18 | end 19 | 20 | it "raises without args" do 21 | assert_raises(ArgumentError) { { the: 'borg' }.dig() } 22 | end 23 | 24 | it "handles type-mixed deep digging" do 25 | h = {} 26 | h[:foo] = [ { bar: [ 1 ] }, [ obj = Object.new, 'str' ] ] 27 | def obj.dig(*args); [ 42 ] end 28 | 29 | assert_equal([1], h.dig(:foo, 0, :bar)) 30 | assert_equal(1, h.dig(:foo, 0, :bar, 0)) 31 | assert_equal 'str', h.dig(:foo, 1, 1) 32 | # MRI does not recurse values returned from `obj.dig` 33 | assert_equal [42], h.dig(:foo, 1, 0, 0) 34 | assert_equal [42], h.dig(:foo, 1, 0, 0, 10) 35 | end 36 | 37 | it "raises TypeError if an intermediate element does not respond to #dig" do 38 | h = {} 39 | h[:foo] = [ { bar: [ 1 ] }, [ nil, 'str' ] ] 40 | assert_raises(TypeError) { h.dig(:foo, 0, :bar, 0, 0) } 41 | assert_raises(TypeError) { h.dig(:foo, 1, 1, 0) } 42 | end 43 | 44 | it "calls #dig on the result of #[] with the remaining arguments" do 45 | h = { foo: { bar: { baz: 42 } } } 46 | 47 | #TODO? EH?? 48 | # h[:foo].should_receive(:dig).with(:bar, :baz).and_return(42) 49 | 50 | assert_equal 42, h.dig(:foo, :bar, :baz) 51 | end 52 | 53 | end -------------------------------------------------------------------------------- /test/test_230.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | if ruby_version_at_least_2_3_0? 4 | class Test230 < Minitest::Test 5 | our_home_path = File.expand_path("../..", __FILE__) 6 | [Array, Hash, Struct, OpenStruct].each do |klass| 7 | define_method("test_#{klass}_does_not_patch".to_sym) do 8 | source_location = klass.instance_method(:dig).source_location 9 | assert source_location.nil? || !source_location.first.start_with?(our_home_path), "On ruby 2.3.0+, unwanted monkey-patch of #{klass}#dig with dig_rb implementation: #{source_location}" 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /test/test_dig_rb.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | class TestDigRb < Minitest::Test 4 | 5 | 6 | # https://github.com/ruby/ruby/blob/a837be87fdf580ac4fd58c4cb2f1ee16bab11b99/test/ruby/test_array.rb#L2655 7 | def test_array 8 | h = Array[Array[{a: 1}], 0] 9 | assert_equal(1, h.dig(0, 0, :a)) 10 | assert_nil(h.dig(2, 0)) 11 | assert_raises(TypeError) {h.dig(1, 0)} 12 | end 13 | 14 | # http://ruby-doc.org/core-2.3.0/Array.html#method-i-dig 15 | def test_array_examples 16 | a = [[1, [2, 3]]] 17 | assert_equal(3, a.dig(0, 1, 1)) 18 | assert_equal(nil, a.dig(1, 2, 3)) 19 | 20 | # The docs lie, in 2.3.0 this actually raises: 21 | # TypeError: Fixnum does not have #dig method 22 | # assert_equal(nil, a.dig(0, 0, 0)) 23 | e = assert_raises(TypeError) { a.dig(0, 0, 0) } 24 | assert_equal("Fixnum does not have #dig method", e.message) 25 | 26 | assert_equal(:bar, [42, {foo: :bar}].dig(1, :foo)) 27 | end 28 | 29 | # https://github.com/ruby/ruby/blob/a837be87fdf580ac4fd58c4cb2f1ee16bab11b99/test/ruby/test_hash.rb#L1306 30 | def test_hash 31 | h = Hash[a: Hash[b: [1, 2, 3]], c: 4] 32 | assert_equal(1, h.dig(:a, :b, 0)) 33 | assert_nil(h.dig(:b, 1)) 34 | assert_raises(TypeError) {h.dig(:c, 1)} 35 | o = Object.new 36 | def o.dig(*args) 37 | {dug: args} 38 | end 39 | h[:d] = o 40 | assert_equal({dug: [:foo, :bar]}, h.dig(:d, :foo, :bar)) 41 | end 42 | 43 | # http://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig 44 | def test_hash_examples 45 | h = { foo: {bar: {baz: 1}}} 46 | assert_equal(1, h.dig(:foo, :bar, :baz)) 47 | assert_nil(h.dig(:foo, :zot, :xyz)) 48 | 49 | g = { foo: [10, 11, 12] } 50 | assert_equal(11, g.dig(:foo, 1)) 51 | end 52 | 53 | # https://github.com/ruby/ruby/blob/a837be87fdf580ac4fd58c4cb2f1ee16bab11b99/test/ruby/test_struct.rb#L363 54 | def test_struct 55 | klass = Struct.new(:a) 56 | o = klass.new(klass.new({b: [1, 2, 3]})) 57 | assert_equal(1, o.dig(:a, :a, :b, 0)) 58 | assert_nil(o.dig(:b, 0)) 59 | end 60 | 61 | # http://ruby-doc.org/core-2.3.0/Struct.html#method-i-dig 62 | def test_struct_examples 63 | klass = Struct.new(:a) 64 | o = klass.new(klass.new({b: [1, 2, 3]})) 65 | 66 | assert_equal(1, o.dig(:a, :a, :b, 0)) 67 | assert_nil(o.dig(:b, 0)) 68 | end 69 | 70 | # Not covered by any tests, but actual behavior. 71 | # https://github.com/jrochkind/dig_rb/issues/5 72 | def test_struct_supports_array_access 73 | klass = Struct.new(:a, :b) 74 | o = klass.new(:first, :second) 75 | 76 | assert_equal(:second, o.dig(1)) 77 | assert_nil(o.dig(2)) 78 | 79 | e = assert_raises(TypeError) { o.dig(Object.new) } 80 | end 81 | 82 | # https://github.com/ruby/ruby/blob/a837be87fdf580ac4fd58c4cb2f1ee16bab11b99/test/ostruct/test_ostruct.rb#L112 83 | def test_ostruct 84 | os1 = OpenStruct.new 85 | os2 = OpenStruct.new 86 | os1.child = os2 87 | os2.foo = :bar 88 | os2.child = [42] 89 | assert_equal :bar, os1.dig("child", :foo) 90 | assert_nil os1.dig("parent", :foo) 91 | e = assert_raises(TypeError) { os1.dig("child", 0) } 92 | # tested in 2.3.0: 93 | assert_equal("0 is not a symbol nor a string", e.message) 94 | end 95 | 96 | # http://ruby-doc.org/stdlib-2.3.0/libdoc/ostruct/rdoc/OpenStruct.html#method-i-dig 97 | def test_ostruct_examples 98 | address = OpenStruct.new('city' => "Anytown NC", 'zip' => 12345) 99 | person = OpenStruct.new('name' => 'John Smith', 'address' => address) 100 | 101 | assert_equal(12345, person.dig(:address, 'zip')) 102 | assert_nil(person.dig(:business_address, 'zip')) 103 | end 104 | 105 | 106 | end 107 | --------------------------------------------------------------------------------