I would like to write a module that provides active record like functionality on an array instance variable.
Examples of its use would be
x = Container.new
x.include(ContainerModule)
x.elements << Element.new
x.elements.find id
module ContainerModule
def initialize(*args)
#elements = []
class << #elements
def <<(element)
#do something with the Container...
super(element)
end
def find(id)
#find an element using the Container's id
self
#=> #<Array..> but I need #<Container..>
end
end
super(*args)
end
end
The problem is that I need the Container object within these methods. Any reference to self will return the Array, not the Container object.
Is there any way to do this?
Thanks!
Would something like this work for you?
class Container
attr_accessor :elements
def initialize
#elements = ContainerElements.new
end
end
class ContainerElements < Array
def find_by_id(id)
self.find {|g| g.id == id }
end
end
So i create a container-class, and a ContainerElements that inherits from Array, with an added (specific) find_by_id method.
If you really want to call it find you need to alias it.
Example code would be:
class ElemWithId
attr_accessor :id
def initialize(value)
#id = value
end
end
cc = Container.new
cc.elements << ElemWithId.new(1)
cc.elements << ElemWithId.new(5)
puts "elements = #{cc.elements} "
puts "Finding: #{cc.elements.find_by_id(5)} "
Hope this helps ...
Your best approach may be to work with the Hash class, which has operations like finding by id already. Particularly, the fetch method may help you out.
Related
I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)
So I got introduced to using PORO instead of AR object for abstraction and size reduction.
But I have so many AR tables didn't make sense to put so much time to build a PORO class for each and every. Would take like a hour or two!! So instead I spent many hours thinking about how can I make this simpler.
And this is what I ended up making:
class BasePORO
def initialize(obj, immutable = true)
self.class::ATTRIBUTES.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
instance_eval("undef #{attr}=") if immutable
end
end
end
class UserPORO < BasePORO
# or plug your own attributes
ATTRIBUTES = User.new.attributes.keys.map(&:to_sym).freeze
attr_accessor(*ATTRIBUTES)
end
But I can't somehow move the attr_accessor into the Base class or even ATTRIBUTES when not given explicitly. Not sure if its possible even.
Can I somehow move attr_accessor and default ATTRIBUTES into the main BasePORO class?
Any pointers or feedback is welcome.
As suggested in the comments, OpenStruct can do most of the heavy lifting for you. One thing to note is that if you don't freeze it, then after it's initialization you'll be able to add more attributes to it throughout its lifetime, e.g.:
struct = OpenStruct.new(name: "Joe", age: 20)
struct.email = "joe#example.com" # this works
p struct.email # => "joe#example.com"
(so essentially it works like a Hash with object-like interface)
This behavior may be undesired. And if you do freeze the struct, it won't allow any more attributes definition, but then you'd also lose the ability to override existing values (which I think you want to do in cases when someone sets immutable to false).
For the immutable flag to work as I understand you to expect it, I'd create a class that uses OpenStruct under its hood, for example like this:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
#data = OpenStruct.new(obj.attributes)
obj.attributes.keys.each do |attr|
self.class.define_method(attr.to_sym) do
#data.send(attr.to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
#data.send("#{attr}=".to_sym, new_value)
end
end
end
end
end
class UserPORO < BasePORO
end
BTW, if you insisted on having a solution similar to the one shown in the question, then you could achieve this with something like that:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
attributes.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
self.class.define_method(attr.to_sym) do
instance_variable_get("##{attr}".to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
instance_variable_set("##{attr}".to_sym, new_value)
end
end
end
end
private
# default attributes
def attributes
[:id]
end
end
class UserPORO < BasePORO
private
# overriding default attributes from BasePORO
def attributes
User.new.attributes.keys.map(&:to_sym).freeze
end
end
So this is what actually ended up with:
class BaseStruct < OpenStruct
def initialize(model, immutable: true, only: [], includes: [])
if only.empty?
hash = model.attributes
else
hash = model.attributes.slice(*only.map!(&:to_s))
end
includes.each do |i|
relation = model.public_send(i)
if relation.respond_to?(:each)
hash[i.to_s] = relation.map{|r| OpenStruct.new(r.attributes).freeze}
else
hash[i.to_s] = OpenStruct.new(relation.attributes).freeze
end
end
super(hash)
self.freeze if immutable
end
end
Feel free to critique or suggest improvements.
I am taking ruby-kickstart (Josh Cheek) challenges and even though I managed to pass all the test there is one thing I cannot understand.
In the exercise you are being asked to override the << method for an instance variable. Specifically here is what you have to do:
In Ruby on Rails, when a person goes to a URL on your site, your
application looks at the url, and maps it to a controller method to
handle the request
My boss wanted to be able to specify what CSS class the body of the
HTML output should have, based on which controller method was being
accessed. It fell to me to provide a method, which, when invoked,
would return a String that could handle the request There are a few
nuances, though. The String you return must be retained during the
object's entire life The method must be able to be called multiple
times The String you return should know how to add new classes: each
class is separated by a space The only method you need to worry about
being invoked is the << method.
(plus a few other irrelevant things)
EXAMPLE:
controller = ApplicationController.new
controller.body_class
#=> ""
controller.body_class << 'admin'
controller.body_class
#=> "admin"
controller.body_class << 'category'
controller.body_class
#=> "admin category"
controller.body_class << 'page' << 'order'
controller.body_class
#=> "admin category page order"
My working solution:
class ApplicationController
def initialize
#body_class = ""
end
def body_class
def #body_class.<<(str)
puts "self is:"+self
return self if self=~/\b#{Regexp.escape(str)}\b/
if self==""
self.concat(str)
else
self.concat(" ")
self.concat(str)
end
end
return #body_class
end
end
Everything works perfectly fine.
But an earlier solution I gave (and it didn't work) was the following
class ApplicationController
attr_accessor :body_class
def initialize
#body_class = ""
end
def #body_class.<<(str)
puts "self is:"+self
return self if self=~/\b#{Regexp.escape(str)}\b/
if self==""
self.concat(str)
else
self.concat(" ")
self.concat(str)
end
end
def body_class #Even this for the way I work it out on my mind is unnecessary
return #body_class
end
end
When someone runs on the second not-working sollution the following
obj = ApplicationController.new
obj.body_class << "Hi"
The << method is not being overridden by the object's singleton.
I do not understand why I have to wrap the singleton methods inside the body_class method. (Mind that in the second solution there is an attr_accessor.
Could anyone enlighten me please!
Thanks!
I do not understand why I have to wrap the singleton methods inside the body_class method.
To access the correct instance variable. When you attempt to override it outside of method, you're in the class context. This code runs at class load time. No instances have been created yet. (And even if instances did exist at this point, #body_class instance variable belongs to class ApplicationController, which is itself an instance of class Class).
You need instance context.
Also I am pretty sure that this problem can be solved without any method patching voodoo. Just provide a different object (conforming to the same API. This is called "duck typing").
class ApplicationController
def body_class
#body_class ||= CompositeClass.new
end
class CompositeClass
def initialize
#classes = []
end
def <<(new_class)
#classes << new_class
end
# I imagine, this is what is ultimately called on ApplicationController#body_class,
# when it's time to render the class in the html.
def to_s
#classes.join(' ')
end
end
end
Didn't test this code, naturally.
BTW, the proper way to do it is to explicitly extend the instance variable:
class A
attr_reader :body_class
def initialize
#body_class = "".extend(Module.new do
def <<(other)
return self if self[/\b#{Regexp.escape(other)}\b/]
concat(' ') unless empty?
concat(other)
end
end)
end
end
How one can add syntactic sugar similar to rails "add to collection" << operator, i.e.
#object.collection << item
I was trying to do
class Object
def collection<<(item)
...
end
end
but it does not work. Optionally I would like to define my own "operators" on attributes.
Note - i am aware hot to use def <<(value) but it works for the whole object not for its attribute.
#object.collection << item
Let's take this apart.
#object - well, some object.
collection - when #object is sent
this message it returns something.
<< - this message is sent to the
object that was returned from the collection message.
item -
parameter to << message.
Example
class Foo
def << val
puts "someone pushed #{val} to me"
end
end
class Bar
def collection
#foo ||= Foo.new
end
end
b = Bar.new
b.collection << 'item'
# someone pushed item to me
By the way, these forms do the same thing and produce the same output.
b.collection << 'item'
b.send(:collection).send(:<<, 'item')
b.collection.<<('item')
b.collection.<< 'item'
This isn't possible based on how Ruby works. You will need your collection method to return an object which has your custom << method on it.
<< is a method of Array, so this works in plain Ruby:
def MyClass
def initialize
#collection = []
end
def collection
#collection
end
end
MyClass.new.collection << 'foo'
For cruft-removal purposes I would like to log whenever a method from one of my AR models is called.
I can get get all those classes with something like this:
subclasses = [] ; ObjectSpace.each_object(Module) {|m| subclasses << m if m.ancestors.include? ActiveRecord::Base } ; subclasses.map(&:name)
But then I need a list of only the methods defined on those classes (instance and class methods), and a way to inject a logger statement in them.
The result would be the equivalent of inserting this into every method
def foo
logger.info "#{class.name} - #{__method__}"
# ...
end
def self.foo
logger.info "#{name} - #{__method__}"
# ...
end
How can I do that without actually adding it to every single method?
Some awesome meta perhaps?
If you want only the methods defined in the class you can do this:
>> Project.instance_methods
=> ["const_get", "validates_associated", "before_destroy_callback_chain", "reset_mocha", "parent_name", "inspect", "slug_normalizer_block", "set_sequence_name", "require_library_or_gem", "method_exists?", "valid_keys_for_has_and_belongs_to_many_association=", "table_name=", "validate_find_options_without_friendly", "quoted_table_name" (another 100 or so methods)]
Only the methods defined in your class
>> Project.instance_methods(false)
=> ["featured_asset", "category_list", "before_save_associated_records_for_slugs", "asset_ids", "primary_asset", "friendly_id_options", "description", "description_plain"]
You should be using Aspect Oriented Programming pattern for this. In Ruby Aquarium gem provides the AOP DSL.
Create a log_method_initializer.rb in config/initializers/ directory.
require 'aquarium'
Aspect.new(:around, :calls_to => :all_methods,
:in_types => [ActiveRecord::Base] ) do |join_point, object, *args|
log "Entering: #{join_point.target_type.name}##{join_point.method_name}"
result = join_point.proceed
log "Leaving: #{join_point.target_type.name}##{join_point.method_name}"
result
end
Every method calls of classes inherited from ActiveRecord::Base will be logged.
You have
AR::Base.instance_methods
and
AR::Base.class_eval "some string"
so you can probably use them to put a header on every existing method.
For instance method call you can use this proxy pattern:
class BlankSlate
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
end
class MyProxy < BlankSlate
def initialize(obj, &proc)
#proc = proc
#obj = obj
end
def method_missing(sym, *args, &block)
#proc.call(#obj,sym, *args)
#obj.__send__(sym, *args, &block)
end
end
Example:
cust = Customer.first
cust = MyProxy.new(cust) do |obj, method_name, *args|
ActiveRecord::Base.logger.info "#{obj.class}##{method_name}"
end
cust.city
# This will log:
# Customer#city
This is inspired from: http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
You will need to find a way to apply this pattern on ActiveRecord::Base object creation.
For Aquarium, seems like adding method_options => :exclude_ancestor_methods does the trick.
I had the stack too deep problem as well.
Source
http://andrzejonsoftware.blogspot.com/2011/08/tracing-aspect-for-rails-application.html