I have a class with a hash of lambda's pointing to methods in the class.
Thing.new.call
=> undefined local variable or method `do_foo' for Thing:Class
I was expecting
Thing.new.call
=> foo
=> bar
The lambda's seem to be calling do_foo and do_bar as class methods instead of instance methods. Can I fix this? I don't want to use send(command) as the commands can be updated by a user and can be a hacking risk.
Link to a replit
class Thing
def initialize
#commands = [:foo, :bar]
end
SAFE_COMMANDS = {
foo: -> { do_foo },
bar: -> { do_bar }
}
def call
#commands.each { |command| SAFE_COMMANDS[command].call }
end
private
def do_foo
puts 'foo'
end
def do_bar
puts 'bar'
end
end
The problem here is your SAFE_COMMANDS array and the lambdas in it are being defined in class context, therefore it is trying to call class methods.
You can pass the instance to the lambdas so that you can call its instance methods.
class Thing
def initialize
#commands = [:foo, :bar]
end
SAFE_COMMANDS = {
foo: -> (obj){ obj.do_foo },
bar: -> (obj){ obj.do_bar }
}
def call
#commands.each { |command| SAFE_COMMANDS[command].call(self) }
end
private
def do_foo
puts 'foo'
end
def do_bar
puts 'bar'
end
end
However you'll get the following error:
private method `do_foo' called for #<Thing:0x00007fdc018399f8 #commands=[:foo, :bar]> (NoMethodError)
This is because those methods are private and therefore inaccessible by the lambdas.
So here's what I'd recommend instead. Use #send but filter the commands first and raise an exception (or something similar) if an illegal command is issued.
class Thing
def initialize
#commands = [:do_foo, :do_bar, :do_bad_stuff, :do_really_bad_stuff]
end
SAFE_COMMANDS = %i{
do_foo
do_bar
}
def call
illegal_commands = #commands - SAFE_COMMANDS
raise("illegal commands: #{illegal_commands.join(', ')}") unless illegal_commands.empty?
#commands.each { |command| self.send(command) }
end
private
def do_foo
puts 'foo'
end
def do_bar
puts 'bar'
end
end
This creates and array of any commands not in your SAFE_COMMANDS array. If that array is empty then we've only been passed legal commands and we proceed. Otherwise we raise an exception, and nicely format those illegal commands to tell the user they 'done screwed up.'
illegal commands: do_bad_stuff, do_really_bad_stuff (RuntimeError)
But I'd actually recommend checking this at initialization time. I assume you are going to eventually pass these commands to the instance somehow so I made that change.
class Thing
def initialize( *commands )
commands = commands.map(&:to_sym)
illegal_commands = commands - SAFE_COMMANDS
raise("illegal commands: #{illegal_commands.join(', ')}") unless illegal_commands.empty?
#commands = commands
end
SAFE_COMMANDS = %i{
do_foo
do_bar
}
def call
#commands.each { |command| self.send(command) }
end
private
def do_foo
puts 'foo'
end
def do_bar
puts 'bar'
end
end
# the exception is raised here
thing = Thing.new(:do_foo, :do_bar, :do_bad_stuff, :do_really_bad_stuff)
# rather than here
thing.call
I think filtering the command list rather than shenanigans with lambdas is way safer security wise, and way less complicated.
Related
I want to pass instance variables to a method, which then modifies them. This is because I have the same logic for different instance variables. Is this possible? I haven't got it working.
class A
def update_ivar()
update(#ivar)
end
def update(var)
var = 1
end
def print_ivar()
puts #ivar
puts #ivar == nil
end
end
a = A.new
a.update_ivar()
a.print_ivar()
Output
true
You can use instance_variable_set like this:
class A
def update_ivar
update(:#ivar) # Note the symbolized name here, it is not the variable itself
end
def update(var_name)
instance_variable_set(var_name, 1)
end
def print_ivar
puts #ivar
puts #ivar == nil
end
end
a = A.new
a.update_ivar
a.print_ivar
#=> 1
#=> false
I personally wouldn't like such a pattern because it leads to hard to read and understand code. But is it a code smell? That certainly depends on your application and your exact use case.
Instead of defining a scope in a class like this:
scope :first_user, -> { first }
And calling it like this: User.first_user
I would like to define a block in another class, that can be called on the user class and works like a Scope:
This code is not working but it should signalize what behaviour I want to achieve:
class Manage
def get_first_user
User.&first_added
end
def first_added
Proc.new { first }
end
end
When I run this code:
a = Manage.new
a.get_first_user
it says me, & undefined method for User. How can I execute the Block defined in first_added on the User model?
How can I in general call a block on a class? Thanks
If I understand your question correctly, you can use class_eval:
>> foo = Proc.new { count }
=> #<Proc:0x007f1aa7cacfd8#(pry):30>
>> Buyer.class_eval(&foo)
(30.6ms) SELECT COUNT(*) FROM "buyers"
=> 1234
Or with your example:
class Manage
def get_first_user
User.class_eval(&first_added)
end
def first_added
Proc.new { first }
end
end
It is not what you want but maybe this will be helpful.
I am not sure if one can call proc on something. I think one can only call proc with something, i.t. in your case passing User as parameter.
def get_first_user
wrap User, &first_added
end
def first_added
Proc.new { |model| model.where(...) }
end
private
def wrap(model, &block)
yield model
end
Here's three ways to call first on User from Manager using:
Object#send:
http://ruby-doc.org/core-2.3.1/Object.html#method-i-send
Module#class_eval:
http://ruby-doc.org/core-2.3.1/Module.html#method-i-class_eval
File manage.rb:
class User
def self.first
puts 'record would probably go here'
end
def self.yielder
print "via yielder => "
self.send(yield) if block_given?
end
def self.blocker(&block)
print "via blocker => "
self.send(block.call) if block_given?
end
def self.evaller(&block)
print "via evaller => "
self.class_eval(block.call) if block_given?
end
end
class Manage
def get_first_user
User.yielder(&first_added)
User.blocker(&first_added)
User.evaller(&first_added)
end
def first_added
Proc.new {"first"}
end
end
a = Manage.new
a.get_first_user
Output:
$ ruby manage.rb
via yielder => record would probably go here
via blocker => record would probably go here
via evaller => record would probably go here
I'm trying to write a rspec test for a mixin class. I have the following.
module one
module two
def method
method_details = super
if method_details.a && method_details.b
something
elsif method_details.b
another thing
else
last thing
end
end
end
end
Now I have mocked the "method" object that will be passed to the class.
But I'm struggling to access the super method.
I did,
let(:dummy_class) { Class.new { include one::two } }
How to pass the mocked method object to this dummy class?
How do I go about testing this? New to ruby, can someone show me a direction with this.
Thanks in advance.
UPDATE:
I tried,
let(:dummy_class) {
Class.new { |d|
include one::two
d.method = method_details
}
}
let (:method_details){
'different attributes'
}
still doesn't work. I get undefined local variable or method method_details for #<Class:0x007fc9a49cee18>
I personally test mixing with the class. Because the mixing (module) itself has no meaning unless its attached to a class/object.
Ex:
module SayName
def say_name
p 'say name'
end
end
class User
include SayName
end
So I believe you should test your module with attached to the relevant class / object.
How ever this is a different perspective on testing mixings
HTH
I think that in your specs, you'll need to explicitly provide a super class definition for when super is called in #method as "you can't mock super and you shouldn't".
I've attempted to spec out all three of your scenarios with the following minor changes:
Changed your example code slightly to become valid Ruby
Changed #method to #the_method so it doesn't conflict with Object#method
Used OpenStruct to represent the object that super returns, because all I know is that it's an object that has methods #a and #b. You can change that out as appropriate for your real specs
Copy and paste the class and specs below into a file and give them a try:
module One
module Two
def the_method
method_details = super
if method_details.a && method_details.b
'something'
elsif method_details.b
'another thing'
else
'last thing'
end
end
end
end
RSpec.describe One::Two do
require 'ostruct'
let(:one_twoable) { Class.new(super_class) { include One::Two }.new }
describe '#the_method' do
let(:the_method) { one_twoable.the_method }
context 'when method_details#a && method_details#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: true, b: true)
end
end
end
it 'is "something"' do
expect(the_method).to eq('something')
end
end
context 'when just method#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: true)
end
end
end
it 'is "another thing"' do
expect(the_method).to eq('another thing')
end
end
context 'when neither of the above' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: false)
end
end
end
it 'is "last thing"' do
expect(the_method).to eq('last thing')
end
end
end
end
I have a class Notification::Pseudo in my rails application that has a custom initialize method. I would like this method to capture the output of the block that was passed to new and use that as the value for #message
class Notification::Pseudo
attr_accessor :message
def initialize(&block)
#message = begin
capture(&block) if block_given?
end || ""
end
end
In my view I then have something like
- notification = Notification::Pseudo.new do
This is a test!
This doesn't work though. This gives me the error ArgumentError: wrong number of arguments (0 for 1).
What is wrong w/ my initializer?
capture method you are calling is defined on Kernel module. You want to call capture from ActionView::Helpers::CaptureHelper module. It is automaticaly included into view context and you need to run it in this context so you need:
class Notification::Pseudo
attr_accessor :message
def initialize(vc, &block)
#message = begin
vc.capture(&block) if block_given?
end || ""
end
end
#In your view
- notification = Notification::Pseudo.new self do
This is a test!
UPDATE:
To make it work also outside of the view, do:
class Notification::Pseudo
attr_accessor :message
def initialize(vc = nil, &block)
#message = begin
return unless block_given?
vc ? vc.capture(&block) : block.call
end || ""
end
end
This is in my application helper:
def call_me
"blah"
end
P = Proc.new { call_me }
def test_me
P.call
end
If I then do this in my view:
<%= test_me %>
I get an error saying that call_me is an undefined method.
How do I get the Proc to be able to call call_me? require doesn't do it. Prefixing with ApplicationHelper::call_me doesn't either. :(
This works, but I really don't like it since test_me will be called lots of times and in reality there are many many more Procs:
def test_me
p = Proc.new { call_me }
p.call
end
It should work as is in Ruby 1.9.2, but in earlier versions you can pass your helper as an argument to the Proc:
P = Proc.new { |helper| helper.call_me }
def test_me
P.call self
end
Since call_me is an instance method on your helper, you need to invoke it with an instance.
I think you were looking for this?
def call_me
Proc.new
end
proc = call_me { "blah" }
proc.call #=> "blah"
you have to pass call_me in. gets kind of convoluted...
p = Proc.new { |m| m.call }
def test_me
p.call(call_me)
end
I'm not entirely sure what your aim here is in the larger sense so this is more or less a stab in the dark...
The most pragmatic way to do this is to make the module methods static, and then simply using them wherever you want:
module Sample::SillyModule
def self.say_hello(my_cool_var)
puts "Hello #{my_cool_var}!"
end
end
proc do
Sample::SillyModule.say_hello("stackoverflow")
end