Serialize With Options Configuration Blocks - ruby-on-rails

I'm using serialize_with_options ( http://www.viget.com/extend/simple-apis-using-serializewithoptions/ ) in a rails project and have been using named blocks for rendering as per the example on the linked page:
class Speaker < ActiveRecord::Base
# ...
serialize_with_options do
methods :average_rating, :avatar_url
except :email, :claim_code
includes :talks
end
serialize_with_options :with_email do
methods :average_rating, :avatar_url
except :claim_code
includes :talks
end
end
Then I can call the second block configuration with #speaker.to_xml(:with_email). This works well, however, I'd like to figure out how to call this block when I have an array of objects. For example, the following does not work:
#speakers = Speaker.all
#speakers.to_xml(:with_email)
Which returns a "TypeError: can't dup Symbol" error. This makes sense to me since Array hasn't been configured to use serialize_with_options. How can I get this tag to be passed on to the individual speaker objects when running .to_xml and render all speakers :with_email?

In your above example, #speakers is a Array object. You need to implement / override the to_xml there . Then i should work:
class Array
def to_xml (with_email)
self.each do |element|
element.to_xml(with_email)
end
end
end

Related

Rails: build method breaks loop

I'm was trying to add multiple objects like so:
class Person < ActiveRecord::Base
has_many :interests
...
def add_interests(interest_hashes)
interest_hashes.each do |interest|
Rails.logger.debug "person.apply_interests: interest: #{interest.inspect}"
interests.build(:name => interest.name, :category => interest.category)
end
save!
end
...
end
However in the log when calling <some person>.add_interests(<some hashes>) all I see is the first hash - no error or exception. If I remove the build method the loop works as expected.
What is happening when calling the build method?
What's a better way to achieve what I'm trying?
Edit:
interest_hashes.inspect output example:
[{"category"=>"Interest", "name"=>"Formula One"}, {"category"=>"Musical instrument", "name"=>"Guitar"}]
You should get a NoMethodError when calling name and category on interest, since hashes are accessed using the [] method. Replace
interest.name
with
interest["name"]
Or use an Struct, which may be preferable.

Dynamically defining instance method within an instance method

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

Model code to module: wrong number of args, class/ instance method?

I am trying to move some model code to a module.
The original model method:
I am trying to move some model code to a module.
The original model method:
class Book < ActiveRecord::Base
def book_royalty(period='enddate', basis="Net receipts")
#stuff
end
end
So I add
include Calculation
and move the method to a module:
module Calculation
def book_royalty(period='enddate', basis="Net receipts")
#stuff
end
end
But now I'm getting
wrong number of arguments (2 for 0)
This is the error I also get if I make the method in the book.rb model a class method i.e. if I make the method name self.book_royalty(args).
Am I inadvertently making the methods moved to the module class methods? I'm using include in book.rb, not extend. How can I get the parent model to successfully include the module's methods?
Edit
book_royalty is called in the Royaltystatement model.
book.rb:
attr_accessor :book_royalty
royaltystatement.rb:
def initialize_arrays
#royalty = []
...
end
def sum_at_book_level(contract)
contract.books.where(:exclude_from_royalty_calc => false).each do |book|
book.book_royalty("enddate", basis)
#royalty.push(book.book_royalty("enddate", basis))
# etc
end
Explanation:
Your module defines a method book_royalty that takes two arguments. Then, a couple of lines after the inclusion of that module you use class macro attr_accessor which defines two methods,
def book_royalty
#book_royalty
end
def book_royalty= val
#book_royalty = val
end
This effectively overwrites your book_royalty from the module. Now it accepts no arguments. Hence the error
wrong number of arguments (2 for 0)
when trying to execute line
book.book_royalty("enddate", basis)
You don't need attr_accessor or anything else in order to use a method from included module. It becomes available automatically.

Rails AntiPatterns book - Doubts on composition

I'm reading the Rails AntiPatterns book, which I'm enjoying a lot. At one point, the author talks about the goodness of composition and it gives an example where an Order class gives the responsibility of conversion (to other formats) to another class, called OrderConverter. The classes are defined as:
class Order < ActiveRecord::Base
def converter
OrderConverter.new(self)
end
end
class OrderConverter
attr_reader :order
def initialize(order)
#order = order
end
def to_xml
# ...
end
def to_json
# ...
end
...
end
And then the author says: "In this way, you give the conversion methods their own home, inside a separate and easily testable class. Exporting the PDF version of an order is now just a matter of call-ing the following:"
#order.converter.to_pdf
Regarding to this, my questions are:
Why do you think that order object is preceded by an #? Shouldn't it be created as:
order = Order.new
And then convert by doing:
order.converter.to_pdf
Why is the attr_reader :order line needed in the OrderConverter? It's so we can access the order from an OrderConverter object? Is it needed to be able to do
order.converter.to_pdf ? We could do that without that attr_reader right?
An instance of Order is passed to the initialize method and stored as an instance variable (using the # syntax : #order). This way, this variable can be accessed from other methods in the converter (the variable has the instance scope) :
class OrderConverter
def to_pdf
#order.items.each do |item|
# Write the order items to the PDF
end
end
end
The attr_reader is not strictly required, but is a convenient way to access the Order object from other methods :
class OrderConverter
def to_pdf
order.items.each do |item|
# Write the order items to the PDF
end
end
end
It will also allow you to get the reference to the order out of any converter instance :
converter.order
The # on the front of the variable makes it an instance variable. If it wasn't there the variable would just be a local variable. I'm guessing that since this is a book about Rails, it's assuming that this code would be in a controller. Variables that controllers want to share across methods or expose in their views need to be instance variables. If this is the case #order was probably created either via parameters from a request or with values pulled from the database.
This probably isn't that significant though, both his example and your example work - I think the author was just showing how a call to the OrderConverter would look, and ignored how the Order object got created.
attr_reader :order creates a "getter" method for the #order instance variable in OrderConverter - it's not needed for to_pdf - it would be used to get the Order back out of the OrderConverter via converter.order. I don't see any need to have this in the code you've given so far, but maybe there's some need for it later.

Ruby/Rails: Is it possible to execute a default method when calling an instance (#instance == #instance.all IF "all" is the default method)?

I understand my question is a bit vague but I don't know how else to describe it. I've asked in numerous places and no one seems to understand why I want to do this. But please bear with me, and I'll explain why I want something like this.
I'm using Liquid Templates to allow users to make some dynamic pages on my site. And for those that don't know, Liquid uses a class of theirs called LiquidDrop to expose certain items to the user. Any method in the drop can be called by the Liquid template.
class PageDrop < Liquid::Drop
def initialize(page)
#page = page
end
def name
#page.name
end
def children
PagesDrop.new(#page.children)
end
end
class PagesDrop < Liquid::Drop
def initialize(pages)
#pages = pages
end
def group_by
GroupByDrop.new(#pages)
end
def all
#pages.all
end
def size
#pages.size
end
end
For example, I want to be able to do this:
#page_drop = PageDrop.new(#page)
#page_drop.children # to get an array of children
instead of
#page_drop.children.all
Why do I have a pages drop?
Because I want to be able to cleanly split up the methods I can do to an array of pages, and methods I can do to a single page. This allows me to group pages like so:
#page_drop.children.group_by.some_method_here_that_the_group_drop_contains
To make it simpler for my users, I don't want them to have to think about adding "all" or not to a drop instance to get the "default" object/s that it contains. To reiterate:
#pages_drop = PagesDrop.new(Page.all)
#pages_drop == #pages_drop.pages #I want this to be true, as well as
#pages_drop == #pages_drop.all
Where did I get this idea?
In Rails, a scope (association object) (#person.friends) seems to return the array when you do certain things to it: #person.friends.each, for person in #person.friends
This isn't really possible. When you write #instance you aren't really calling an instance as you describe, you're getting a reference to the object that #instance refers to.
The reason it seems to work with the collections for Rails' associations is that the the association objects are instances of Array that have had some of their methods overridden.
I would consider removing PagesDrop and using the group_by(&:method) syntax if you want a concise way to express groupings. If you do want to keep it then you can get some way towards what you want by implementing each and [] on PagesDrop and having them delegate to #pages. That will let you use #page_drop.children in for loops, for instance.
It looks like you want to implement has_many outside of rails. Will the following work?
class PageDrop < Liquid::Drop
attr_accessor :children
def initialize(page)
#page = page
#children = []
end
def name
#page.name
end
end
This allows you to do the following:
#page_drop = PageDrop.new(#page)
#page_drop.children.size # => 0
#page_drop.children # => []
This also gives you all the standard array functions (group_by, size, each, etc). If you want to add your own methods, create a class that inherits from Array and add your methods there.
class PageArray < Array
def my_method
self.each{|a| puts a}
end
end
class PageDrop < Liquid::Drop
attr_accessor :children
def initialize(page)
#page = page
#children = PageArray.new
end
[...]
end
#page_drop = PageDrop.new(#page)
#page_drop.children.size # => 0
#page_drop.children # => []
#page_drop.children.my_method # Prints all the children
Then any functions you don't define in PageArray fall through to the Ruby Array methods.

Resources