Prevent object from being extended - ruby

I have a module that provides some lazy lookup via dot syntax for hashes:
module DotHash
def method_missing(method_name, *args, &block)
# look for keys...
end
def respond_to_missing?(method_name, _include_all = nil)
# some logic
end
end
I ran into the problem of accidentally extending nil:
# #hash == nil
#hash.extend(DotHash)
and this caused HUGE problems, because now nil has this method_missing logic which messes things up.
I though adding a hook would solve the problem:
module DotHash
def self.extended(base)
return if base.is_a?(Hash)
raise "must be a hash"
end
def method_missing(method_name, *args, &block)
# look for keys...
end
def respond_to_missing?(method_name, _include_all = nil)
# some logic
end
end
And indeed, it throws an error:
[1] pry(main)> nil.extend(DotHash)
RuntimeError: must be a hash
But the logic got added anyway:
[2] pry(main)> nil.foobar
Traceback (most recent call last):
9707: from bin/rails:6:in `<main>'
9706: from /usr/local/bundle/gems/activesupport-5.2.4.3/lib/active_support/dependencies.rb:291:in `require'
9705: from /usr/local/bundle/gems/activesupport-5.2.4.3/lib/active_support/dependencies.rb:257:in `load_dependency'
9704: from /usr/local/bundle/gems/activesupport-5.2.4.3/lib/active_support/dependencies.rb:291:in `block in require'
9703: from /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
9702: from /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
9701: from /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
9700: from /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
... 9695 levels...
4: from /usr/src/app/app/lib/dot_hash.rb:26:in `respond_to_missing?'
3: from /usr/src/app/app/lib/dot_hash.rb:14:in `method_missing'
2: from /usr/src/app/app/lib/dot_hash.rb:26:in `respond_to_missing?'
1: from /usr/src/app/app/lib/dot_hash.rb:14:in `method_missing'
/usr/src/app/app/lib/mapper/dot_hash.rb:26:in `respond_to_missing?': stack level too deep (SystemStackError)
Is there a hook that get's triggered BEFORE the object is extended, and not after?

You can override extend_object: (the docs contain a similar example)
Extends the specified object by adding this module's constants and methods (which are added as singleton methods). This is the callback method used by Object#extend.
module DotHash
def self.extend_object(obj)
raise TypeError, 'must be a hash' unless obj.is_a?(Hash)
super
end
def foo
123
end
end
h = {}
h.extend(DotHash)
h.foo #=> 123
n = nil
n.extend(DotHash) # TypeError: must be a hash
n.foo # NoMethodError: undefined method `foo' for nil:NilClass

Related

instance_eval doesn't work as expected

I'm trying to build a tinny DSL using the approach that Russ Olsen exposes in his book, Eloquent Ruby. However it is not working for me. Let's consider the following code:
class SayHello
def initialize
#message = "Hello."
instance_eval(yield) if yield
end
def say_it
puts #message
end
end
SayHello.new { say_it }
The error I get is:
say_hello.rb:12:in `block in <main>': undefined local variable or method `say_it' for main:Object (NameError)
from say_hello.rb:4:in `initialize'
from say_hello.rb:12:in `new'
from say_hello.rb:12:in `<main>'
But... when you use instance_eval method, the value of self shouldn't be assigned to the object that calls the method?
Thanks in advance!
When the block runs, you want self to be equal to your SayHello instance instead of the main object.
I Googled for "ruby change self for a block" and found a good answer which makes me think you should change your code to:
class SayHello
def initialize(&p)
#message = "Hello."
instance_eval(&p) if block_given?
end
def say_it
puts #message
end
end
SayHello.new { say_it }

Why is rspec not seeing these class methods?

If I have ruby file maze.rb with
class Maze
def self.x
end
def self.y
end
end
and a rspec file with
require 'maze'
describe "A Maze" do
it "exists" do
expect(Maze).to be
end
it " has x-y dimension" do
expect(Maze.x).to be
expect(Maze.y).to be
end
end
Why does the test for Maze.x fail ?
Failures:
1) A Maze has x-y dimension
Failure/Error: expect(Maze.x).to be
expected nil to evaluate to true
# ./spec/maze_spec.rb:8:in `block (2 levels) in <top (required)>'
It is working.
What's happening is that the class level method isn't doing anything and thus returns nil - as opposed to method not found. Simply adding true as the return value resolves this, i.e.
def x
true
end

'block in initialize' uninitialized constant Testdeck NameError (ruby)

class Deck
attr_accessor :all
def initialize
#all = [1,2,3]
end
end
newdeck = Deck.new
puts newdeck.all
class Testdeck
attr_accessor :cards
def initialize
#cards = []
counter = 0
['H','C', 'S', 'D'].product(['2','3','4','5','6','7','8','9','10','J','K','Q','A']).each do |arr|
#cards << Card.new(arr[0], arr[1])
end
end
end
zen = Testdeck.new
puts zen.cards.pop
This code is giving me: 'block in initialize' uninitialized constant Testdeck NameError
All help is appreciated.
This code has no clue what a Card is; the error message you're getting is not an uninitialized constant Testdeck error but an uninitialized constant Testdeck::Card error, where the :: indicates that Ruby is looking inside the Testdeck class for the Card class (not that you need to put it there, Ruby just looks there first).
You need to define the Card class somewhere, and make its constructor take in a string and an integer.

Error no block given when used yield inside a block

I'm getting this error
LocalJumpError: no block given (yield)
from fops.rb:52:in `block (2 levels) in gen_list'
from /home/phanindra/.gem/ruby/1.9.1/gems/mp3info-0.6.18/lib/mp3info.rb:306:in `open'
from fops.rb:51:in `block in gen_list'
from fops.rb:46:in `each'
from fops.rb:46:in `gen_list'
from fops.rb:48:in `block in gen_list'
from fops.rb:46:in `each'
from fops.rb:46:in `gen_list'
from fops.rb:48:in `block in gen_list'
from fops.rb:46:in `each'
from fops.rb:46:in `gen_list'
from fops.rb:48:in `block in gen_list'
from fops.rb:46:in `each'
from fops.rb:46:in `gen_list'
from (irb):2
from /usr/bin/irb:12:in `<main>
when used yield inside another block which is inside a begin statement which is inside a if statement,
for a simple understanding here is the prototype
def test
if 1 then
begin
test2(5) do |x|
yield x
end
rescue
end
end
end
def test2(n)
n.times do |k|
yield k
end
end
test() do |y|
puts y
end
The problem is there is no error with the prototype, it worked fine so I dont understand why I'm getting this error, here is my actual code
require "mp3info"
module MusicTab
module FOps
def self.gen_list(dir)
prev_pwd=Dir.pwd
begin
Dir.chdir(dir)
rescue Errno::EACCES
end
Dir[Dir.pwd+'/*'].each{|x|
if File.directory?(x) then
self.gen_list(x)
else
begin
Mp3Info.open(x) do |y|
yield "#{y.tag.title},#{y.tag.album},#{y.tag.artist},#{x}"
end
rescue Mp3InfoError
end
end
}
Dir.chdir(prev_pwd)
end
end
end
I was testing this code using irb
[phanindra#pahnin musictab]$ irb
irb(main):001:0> load 'fops.rb'
/usr/share/rubygems/rubygems/custom_require.rb:36:in `require': iconv will be deprecated in the future, use String#encode instead.
=> true
irb(main):002:0> MusicTab::FOps.gen_list('/fun/Music') do |l|
irb(main):003:1* puts l
irb(main):004:1> end
Any help?
regards
The problem is that you are calling gen_list recursively and at the call site for the recursive descent there really is no block.
What you can do is either:
capture the block as a proc with a & parameter and then forward it, or
add a block that does another yield to the recursive call
So...
def f1 x, &block
block.call(x)
if x > 0
f1 x - 1, &block
end
end
# ...or...
def f2 x
yield x
if x > 0
f2 x - 1 do |y|
yield y
end
end
end
f1 2 do |q|
p ['b1', q]
end
f2 2 do |q|
p ['b2', q]
end

MiniTest is crashing

I want to overwite send in Object, like so
class Object
##object_send = self.instance_method( :send )
def send *args
##object_send.bind( self ).call *args
end
end
or
class Object
def send *args
__send__ *args
end
end
or
class Object
alias_method :old_send, :send
def send *args
old_send *args
end
end
but all this options cause this error to appear
/opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:871:in `block in process_args': unsupported argument type: Integer (ArgumentError)
from /opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:862:in `new'
from /opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:862:in `process_args'
from /opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:912:in `_run'
from /opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:905:in `run'
from /opt/local/lib/ruby1.9/gems/1.9.1/gems/minitest-2.8.1/lib/minitest/unit.rb:685:in `block in autorun'
Is there anything I can do?
Update: Tried updating to 2.9.1 but that hasn't solved the problem
Without a MWE it is difficult for me to analyze your problem. Perhaps I found a first hint for your problem.
I tried to reconstruct the error, but without success:
class Object
alias_method :old_send, :send
def send *args
old_send *args
end
end
n = 5
puts n.send(:*, 2)
I get 10.
But with blocks I get a problem:
puts n.send(:times){ |i| p i } #-> #<Enumerator:0xb778a8>
With a little modification you can see what happens:
class Object
alias_method :old_send, :send
def send *args
puts "send called with #{args}" #--> [:times]
old_send *args
end
end
n = 5
n.send(:times){ |i| p i }
You get
send called with [:times]
The block is missing. You must add the proc-parameter to your redefinition:
class Object
alias_method :old_send, :send
def send *args, &proc
old_send *args, &proc
end
end
n = 5
n.send(:times){ |i| p i } #-> 1 2 3 4 5

Resources