Ruby: How to make custom DSL accept variables - ruby-on-rails

I have this class:
class Items
def initialize &block
(block.arity < 1 ? (instance_eval &block) : block.call(self)) if block_given?
end
def button_id button_id=nil
unless #button_id.present?
raise "button_id must be supplied" if button_id.nil?
#button_id = button_id
end
#button_id
end
end
Now, when I do this it works:
Items.new do
button_id 1
end
But when I do this, it fails because I think it is not on the same scope:
#button = Button.find(params[:button_id]
Items.new do
button_id #button.id
end
How can fix this to take arguments outside the scope?
Thanks!

Try this:
class Items
def self.dsl
new.tap do |item|
yield item
end
end
def button_id(button_id)
#button_id = button_id
end
end
#button = Button.find(params[:button_id])
item = Items.dsl do |item|
item.button_id(#button.id)
end
puts item.inspect

Turns out all I needed to do was to pass the arguments to the block like this:
Items.new do |item|
item.button_id #button.id
end
Less beautiful DSL but works.

I don't think this is the right use case of DSL, when you can simply assign the attributes by arguments.
class Item
attr_accessor: :button_id
def initialize(args)
button_id = args[:button_id]
end
end
Another problem is in your usage. The instance would be of little value if you don't assign it to a variable
item = Item.new button_id: button_id

Related

How do I create accessors for ActiveRecord models from an array of symbols? [duplicate]

When I run code below it raise error:
implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError).
I am not sure what is the problem.
class Result
def total(*scores)
percentage_calculation(*scores)
end
private
def percentage_calculation(*scores)
puts "Calculation for #{scores.inspect}"
scores.inject {|sum, n| sum + n } * (100.0/80.0)
end
end
def mem_result(obj, method)
anon = class << obj; self; end
anon.class_eval do
mem ||= {}
define_method(method) do |*args|
if mem.has_key?(args)
mem[args]
else
mem[args] = super
end
end
end
end
r = Result.new
mem_result(r, :total)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
The error message is quite descriptive. You need to explicitly pass arguments to super when you call it inside of define_method block:
mem[args] = super(*args)

One method for two models. How to pass name of model as variable to controller?

I have two methods in two different controllers (Posts & Boards). They are almost same. The difference is only model-instance-association name. To DRY this I think to write the method in module, but how to share it between Post and Board?
def init_post_comments
#user = current_user
a = #user.posts.pluck(:id) # not very nice...
b=params[:post_ids] ||= []
b = b.map(&:to_i)
follow = b - a
unfollow = a - b
follow.each do |id| # checkbox just checked
#post = Post.find_by_id(id)
if #post.users.empty?
#post.update_attribute(:new_follow, true)
end
#user.posts << #post
end
unfollow.each do |id| # if checkbox was unchecked
#post = Post.find_by_id(id)
remove_post_from_user(#post)# here we destroy association
end
if follow.size > 0
get_post_comments_data
end
redirect_to :back
end
UPDATE Ok, if I'll move the methods to model's concern how I should work with associations here? Here #user.posts.pluck(:id) and here #user.boards.pluck(:id) with what I can replace posts and boards so it can work with both of them?
So, I did it! I don't know if it's right way, but I DRY this code.
Two controllers:
posts_controller.rb
def init_comments
if Post.comments_manipulator(current_user, params[:post_ids] ||= []) > 0
#posts = Post.new_post_to_follow
code = []
#posts.each do |post|
group = post.group
code = code_constructor('API.call')
end
Post.comments_init(get_request(code), #posts)
end
redirect_to :back
end
boards_controller.rb
def init_comments
if Board.comments_manipulator(current_user, params[:board_ids] ||= []) > 0
#boards = Board.new_board_to_follow
code = []
#boards.each do |board|# подготовка запроса
group = board.group
code = code_constructor('API.call')
end
Board.comments_init(get_request(code), #boards)
end
redirect_to :back
end
As you can see they are absolutely same.
In models board.rb and post.rb - include CommentsInitializer
And in models\concerns
module CommentsInitializer
extend ActiveSupport::Concern
module ClassMethods
def comments_manipulator(user, ids)
relationship = self.name.downcase + 's'
a = user.send(relationship).pluck(:id)
b = ids.map(&:to_i)
follow = b - a
unfollow = a - b
follow.each do |id| # start to follow newly checked obj
#obj = self.find_by_id(id)
if #obj.users.empty?
#obj.update_attribute(:new_follow, true)
end
user.send(relationship) << #obj
end
unfollow.each do |id| # remove from following
#obj = self.find_by_id(id)
remove_assoc_from_user(#obj, user)#destroy relation with current user
end
follow.size
end
def comments_init(comments, objs)
i = 0
objs.each do |obj| # updating comments data
if comments[i]['count'] == 0
obj.update(new_follow: false)
else
obj.update(new_follow: false, last_comment_id: comments[i]['items'][0]['id'])
end
i += 1
end
end
def remove_assoc_from_user(obj, user)
user = user.id
if user
obj.users.delete(user)
end
end
end
My code works. If you know how to make it better please answer!

"method missing" error on Rails/Ruby metaprogramming attempt

I'm trying my first foray into metaprogramming and it's not going very well! It's a Rails 4.1 application and I'm trying to refactor an active record model (User) to combine two methods that are very similar. The original methods are slightly complex DB calls and work as expected.
The original code:
def retweet_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:retweet_count).inject(0) do |sum, n|
sum + ( n.retweet_count || 0 )
end
end
def favorite_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:favorite_count).inject(0) do |sum, n|
sum + ( n.favorite_count || 0 )
end
end
The new code:
twitter_stats_count :retweet, :favorite
private
def twitter_stats_count(*stats)
stats.each do |statistic|
stat = send(statistic).to_s
define_method "#{stat}_count" do |league|
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select("#{stat}_count").inject(0) do |sum, n|
sum + ( n.send("#{stat}_count") || 0 )
end
end
end
end
The error the new code produces when I try to start my rails server:
/Users/kiddo/.rvm/gems/ruby-2.1.0/gems/activerecord-4.1.0.rc2/lib/active_record/dynamic_matchers.rb:26:in `method_missing': undefined method `twitter_stats_count' for User (call 'User.connection' to establish a connection):Class (NoMethodError)
I can't seem to figure out what I'm doing wrong, so any pointers would be much appreciated!
FYI, here's the final code I got working. I mainly went with Holger Just's suggestions, but incorporated aspects from several others, so upvotes all around!
def team_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def self.twitter_stats_count(*stats)
stats.each do |statistic|
stat = statistic.to_s
define_method "#{stat}_count" do |league|
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: self.team_ids(league)
}).sum("#{stat}_count")
end
end
end
twitter_stats_count :retweet, :favorite
There are a couple of issues with your approach:
You call the twitter_stats_count directly on the class, not an instance of the class. As such, the method needs to be a class method. You can define it as a class method with
def self.twitter_stats_count(*stats)
# ...
end
Additionally, you call the method before having it defined. In Ruby, everything (even method definitions) are executed. As such, you can only call methods after they have been defined. Thus, you need to put the call to your twitter_stats_count method after its definition.
That looks quite complicated. If I'm not mistaken, you can reduce the duplication by refactoring your code:
def retweet_count(league)
league_tweets(league).sum(:retweet_count)
end
def favorite_count(league)
league_tweets(league).sum(:favorite_count)
end
def celebrity_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def league_tweets(league)
Tweet.where(
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids(league)
)
end
twitter_stats_count should be a class method, but what you did is make it a instance method, maybe you can try this:
# no private here
def self.twitter_stats_count(*status)
#your codes here
end
You are getting this error because, you have define twitter_stats_count as a private method, You can't call this on self. You have to put it in a instance method, than call it.
Check this.
For example following gives same error:
class Foo
baz
private
def baz
puts "baz called"
end
end
However this will work:
class Foo
def dummy
baz
end
private
def baz
puts "baz called"
end
end
foo = Foo.new
foo.dummy

Why I can not call super in define_method with overloading method?

When I run code below it raise error:
implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError).
I am not sure what is the problem.
class Result
def total(*scores)
percentage_calculation(*scores)
end
private
def percentage_calculation(*scores)
puts "Calculation for #{scores.inspect}"
scores.inject {|sum, n| sum + n } * (100.0/80.0)
end
end
def mem_result(obj, method)
anon = class << obj; self; end
anon.class_eval do
mem ||= {}
define_method(method) do |*args|
if mem.has_key?(args)
mem[args]
else
mem[args] = super
end
end
end
end
r = Result.new
mem_result(r, :total)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
The error message is quite descriptive. You need to explicitly pass arguments to super when you call it inside of define_method block:
mem[args] = super(*args)

Dynamically defined setter methods using define_method?

I use a lot of iterations to define convenience methods in my models, stuff like:
PET_NAMES.each do |pn|
define_method(pn) do
...
...
end
but I've never been able to dynamically define setter methods, ie:
def pet_name=(name)
...
end
using define_method like so:
define_method("pet_name=(name)") do
...
end
Any ideas? Thanks in advance.
Here's a fairly full example of using define_method in a module that you use to extend your class:
module VerboseSetter
def make_verbose_setter(*names)
names.each do |name|
define_method("#{name}=") do |val|
puts "##{name} was set to #{val}"
instance_variable_set("##{name}", val)
end
end
end
end
class Foo
extend VerboseSetter
make_verbose_setter :bar, :quux
end
f = Foo.new
f.bar = 5
f.quux = 10
Output:
#bar was set to 5
#quux was set to 10
You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.
Shoertly if you need it inside one class/module:
I use hash but you can put there array of elements etc.
PETS = {
"cat" => "meyow",
"cow" => "moo",
"dog" => "ruff"
}
def do_smth1(v)
...
end
def do_smth(sound,v)
...
end
#getter
PETS.each{ |k,v| define_method(k){ do_smth1(v) } }
#setter
PETS.each{ |k,v| define_method("#{k}="){|sound| do_smth2(sound, v) }

Resources