In my code base, I have a bunch of objects that all adhere to the same interface, which consists of something like this:
class MyTestClass
def self.perform(foo, bar)
new(foo, bar).perform
end
def initialize(foo, bar)
#foo = foo
#bar = bar
end
def perform
# DO SOMETHING AND CHANGE THE WORLD
end
end
The differentiating factor between the classes is the arity of the self.perform and initialize, plus the body of the #perform class.
So, I'd like to be able to create an ActiveSupport::Concern (or just a regular Module if that would work better) which allowed me to do something like this:
class MyTestClass
inputs :foo, :bar
end
which would then use some meta-programming to define self.perform and initialize of the above methods whose airty would depend on the airty specified by the self.inputs method.
Here is what I have so far:
module Commandable
extend ActiveSupport::Concern
class_methods do
def inputs(*args)
#inputs = args
class_eval %(
class << self
def perform(#{args.join(',')})
new(#{args.join(',')}).perform
end
end
def initialize(#{args.join(',')})
args.each do |arg|
instance_variable_set(##{arg.to_s}) = arg.to_s
end
end
)
#inputs
end
end
end
This seems to get the arity of the methods correct, but I'm having a tough time figuring out how to handle the body of the #initialize methods.
Can anybody help me figure out a way that I can successfully meta-program the body of #initialize so it behaves like the example I provided?
You could use this as body for #initialize:
#{args}.each { |arg| instance_variable_set("#\#{arg}", arg) }
However, I wouldn't string eval it. It usually leads to evil things. That said, here is an implementation which gives an incorrect Foo.method(:perform).arity, but still behaves as you would expect:
module Commandable
def inputs(*arguments)
define_method(:initialize) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
arguments.zip(parameters).each do |argument, parameter|
instance_variable_set("##{argument}", parameter)
end
end
define_singleton_method(:perform) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
new(*parameters).perform
end
end
end
class Foo
extend Commandable
inputs :foo, :bar
def perform
[#foo, #bar]
end
end
Foo.perform 1, 2 # => [1, 2]
You were so close! instance_variable_set takes two arguments, first is the instance variable and second is the value you want to set it to. You also need to get the value of the variable, which you can do using send.
instance_variable_set(##{arg.to_s}, send(arg.to_s))
Related
I'm confused about using "include" vs "extend, after searching for hours all I got is that module methods used with instance of the class including the module, and module methods used with the class itself when the class extending the module of those methods.
but this didn't help me to figure out, why this code give error when commenting the extend module line in "#extend Inventoryable"
while work when uncomment it, here's the code
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
#extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
the output is
store2.rb:52:in `<main>': undefined method `create' for Shirt:Class (NoMethodError)
while when uncomment the "extend Inventoryable" to make the code work:
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
makes the code work and output the following
[#<Shirt:0x0055792cb93890 #attributes={:name=>"MTF", :size=>"L"}>, #<Shirt:0x0055792cb937a0 #attributes={:name=>"MTF", :size=>"M"}>]
it's kinda confusing, but all I need to know, is why I need to extend the module in order to avoid the error ?, and how to edit this code to make it work without the extend method ? , what's left in the code that still depends on the extend ?
When you extend a module, the methods in that module become "class methods"**. So, when you extend Inventoryable, create becomes available as a method on the Shirt class.
When you include a module, the methods in that module become "instance methods"**. So, when you include Inventoryable, create is not available on the Shirt class (but is available on an instance of Shirt).
To make create available on the Shirt class when using include, you can use the included hook. That might look something like:
module Inventoryable
module ClassMethods
def create
puts "create!"
end
end
module InstanceMethods
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.include InstanceMethods
end
end
Then if you do:
class Shirt
include Invetoryable
end
You can do:
> Shirt.create
create!
=> nil
** The ruby purists in the crowd will correctly point out that, in ruby, everything is an instance method and that there are no class methods. That is formally 100% correct, but we'll use the colloquial meaning of class and instance methods here.
When you extend a module in a class, you get the module's methods exposed as class methods but if you include the module then you get the module's method as instance methods, in your example for you to be able to call create method of Inventoryable class you need to invoke it using an instance of Shirt class (if you include the module)
shirt1 = Shirt.new(attributes).create(attributes)
Without more info I can't tell what you are trying to do but you need to redesign the initialize and create methods to decide where or what to do in those methods.
I'll try to explain it using a simple example
module A
def test
puts "ok"
end
end
class B
include A
end
class C
extend A
end
puts C.test # here you invoke the method against the class itself
puts B.new.test #here you create an instance to do it
Hope it helps.
At the end of the day, it's really simple:
C.include(M) makes the current superclass of C the superclass of M and M the superclass of C. In other words, it inserts M into C's ancestry chain.
obj.extend(M) is (roughly) the same as obj.singleton_class.include(M).
I have the following code to represent different Value Objects in Ruby. The only thing that changes between different classes is the INITIALIZATION_ATTRIBUTES array, which represents the list of attributes of the value object. I can't find a way to DRY this code. I tried to use a Module and accessing the included classes' Constants, but I run into the weird Constant lookup behavior described here. Essentially, the Module code is evaluated multiple times and it interprets the constant of the lastly evaluated class and applies its values to all the Value Object classes.
Is there any better alternative? I also tried with a base class, but I couldn't make it work.
module Values
class MaintenanceRegimeSerializer
INITIALIZATION_ATTRIBUTES = [:distance_between_services, :months_between_services]
def self.load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
self.new(*INITIALIZATION_ATTRIBUTES.map {|key| hash[key]})
end
def self.dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
attr_reader *INITIALIZATION_ATTRIBUTES
define_method :initialize do |*args|
raise ArgumentError unless INITIALIZATION_ATTRIBUTES.length == args.length
INITIALIZATION_ATTRIBUTES.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
end
This can be done by layering two modules. The outer module will provide the functionality to initialize the inner module. Because class attributes are used, which are unique for every including class, one including class' attributes can not conflict with another including class' attributes.
module Values
module MaintenanceRegimeSerializer
extend ActiveSupport::Concern
class_methods do
def acts_as_maintenance_regime_serializer(attributes)
# include the inner module
# thereby adding the required methods and class attributes
include JsonMethods
# set the class variables made available by including the inner module
self.serializer_attributes = attributes
end
end
module JsonMethods
extend ActiveSupport::Concern
included do
class_attribute :serializer_attributes
def initialize(*args)
raise ArgumentError unless self.class.serializer_attributes.length == args.length
self.class.serializer_attributes.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
class_methods do
def load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
new(*serializer_attributes.map {|key| hash[key]})
end
def dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
end
end
end
end
# in the including class
class SomeClass
# This might also be put into an initializer patching ActiveRecord::Base
# to avoid having to call this in every class desiring the regime serializer functionalit
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:distance_between_services,
:months_between_services])
end
# in another including class
class SomeOtherClass
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:foo,
:bar])
end
I want to mock a method for every instance of a class.
if I allow_any_instance_of then it works great if instance_count = 1
However if I have many instances of the same class the second instance isn't caught by the mock.
I'm attempting to get a pile of tokens from different sites. But during testing I don't really need "real" tokens. So I plan to mock get_token to return '1111'.
class Foo
def children
[Bar.new, Bar.new] #....
end
def get_tokens
children.map(&:get_token) || []
end
end
so now how do I not mock out the get_tokens?
How about solution like this:
require "spec_helper"
require "ostruct"
class Bar
def get_token
("a".."f").to_a.shuffle.join # simulating randomness
end
end
class Foo
def children
[Bar.new, Bar.new, Bar.new]
end
def get_tokens
children.map(&:get_token) || []
end
end
RSpec.describe Foo do
before do
allow(Bar).to receive(:new).and_return(OpenStruct.new(get_token: "123"))
end
it "produces proper list of tokens" do
expect(Foo.new.get_tokens).to eq ["123", "123", "123"]
end
end
We're stubbing new method on Bar to return something that quacks with get_token (so it behaves like Bar), and it returns a fixed string. This is something you can relay on.
Hope that helps!
I have a several classes, each of which define various statistics.
class MonthlyStat
attr_accessor :cost, :size_in_meters
end
class DailyStat
attr_accessor :cost, :weight
end
I want to create a decorator/presenter for a collection of these objects, that lets me easily access aggregate information about each collection, for example:
class YearDecorator
attr_accessor :objs
def self.[]= *objs
new objs
end
def initialize objs
self.objs = objs
define_helpers
end
def define_helpers
if o=objs.first # assume all objects are the same
o.instance_methods.each do |method_name|
# sums :cost, :size_in_meters, :weight etc
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
end
end
end
YearDecorator[mstat1, mstat2].yearly_cost_sum
Unfortunately define method isn't available from within an instance method.
Replacing this with:
class << self
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
...also fails because the variables method_name and objs which are defined in the instance are no longer available. Is there an idomatic was to accomplish this in ruby?
(EDITED: I get what you're trying to do now.)
Well, I tried the same approaches that you probably did, but ended up having to use eval
class Foo
METHOD_NAMES = [:foo]
def def_foo
METHOD_NAMES.each { |method_name|
eval <<-EOF
def self.#{method_name}
\"#{method_name}\".capitalize
end
EOF
}
end
end
foo=Foo.new
foo.def_foo
p foo.foo # => "Foo"
f2 = Foo.new
p f2.foo # => "undefined method 'foo'..."
I myself will admit it's not the most elegant solution (may not even be the most idiomatic) but I've run into similar situations in the past where the most blunt approach that worked was eval.
I'm curious what you're getting for o.instance_methods. This is a class-level method and isn't generally available on instances of objects, which from what I can tell, is what you're dealing with here.
Anyway, you probably are looking for method_missing, which will define the method dynamically the first time you call it, and will let you send :define_method to the object's class. You don't need to redefine the same instance methods every time you instantiate a new object, so method_missing will allow you to alter the class at runtime only if the called method hasn't already been defined.
Since you're expecting the name of a method from your other classes surrounded by some pattern (i.e., yearly_base_sum would correspond to a base method), I'd recommend writing a method that returns a matching pattern if it finds one. Note: this would NOT involve making a list of methods on the other class - you should still rely on the built-in NoMethodError for cases when one of your objects doesn't know how to respond to message you send it. This keeps your API a bit more flexible, and would be useful in cases where your stats classes might also be modified at runtime.
def method_missing(name, *args, &block)
method_name = matching_method_name(name)
if method_name
self.class.send :define_method, name do |*args|
objs.inject(0) {|obj, sum| sum + obj.send(method_name)}
end
send name, *args, &block
else
super(name, *args, &block)
end
end
def matching_method_name(name)
# ... this part's up to you
end
I'm having trouble removing some duplication I've introduced in a rails plugin.
The code below modifies the find and calculate methods of ActiveRecord in the same way, but I've been unable to remove the duplication.
The find and calculate methods below make use of the super keyword which is one hurdle as the super keyword can only be used to call a method sharing the same name as the calling method, so I can't move the super keyword to a method shared by find and calculate.
So next I tried aliasing the find and calculate class methods from the superclass ActiveRecord, however, I've not been able to get the syntax right for the aliasing. If someone could show me that, it would be a great help.
If you've got a better way entirely of doing this I'd love for you to post that too.
Below I've trimmed the code down a little to highlight the problem:
module Geocodable #:nodoc:
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def acts_as_geocodable(options = {})
extend Geocodable::SingletonMethods
end
end
module SingletonMethods
def find(*args)
some_method_1
super *args.push(options)
some_method_2
end
# TODO: Remove duplication of find above and calculate below.
def calculate(*args)
some_method_1
super *args.push(options)
some_method_2
end
end
end
Your best way to refactor this code is to leave find and calculate unchanged, and add apply the wrapping using a class-level function.
Here's rough sketch, without your module and mixin logic:
class A
def find x
puts 'finding'
end
def calculate x
puts 'calculating'
end
end
class B < A
def self.make_wrapper_method name
define_method name do |*args|
puts "entering"
result = super *args
puts "exiting"
result
end
end
make_wrapper_method :find
make_wrapper_method :calculate
end
Note that this will need to be modified if B has already overridden find or calculate.
To use this code, first make your version work correctly, then modify it to use define_method. (And if you need extremely high performance, you may need to use one of the *_eval functions to create the wrappers instead of define_method.)
This is the option I went for in the end, thanks to emk for guidance to get to this point!
module Geocodable
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def acts_as_geocodable(options = {})
geoify_query_methods
end
private
# This is where the duplication has been removed
def geoify_query_methods
class << self
[:calculate, :find].each do |method_name|
define_method method_name do |*args|
some_method_1
super *args.push(options)
some_method_2
end
end
end
end
end
end
To just alias the find method:
module SingletonMethods
def find(*args)
some_method_1
super *args.push(options)
some_method_2
end
alias :calculate :find
end