I am using Ruby on Rails 3.0.9 and RSpec 2. I am trying to refactoring some spec file in the following way (in order to test with less code similar class object attribute values):
[
:attribute_a,
:attribute_b,
:attribute_c
].each do |attr|
before do
# HERE I would like to set the "current" 'attr' related to the
# class object instance 'attribute_< letter >' (read below for
# more information) each time the iterator is called (note: all
# following attributes are NOT attr_accesible - for that reason
# I use the 'user.attribute_< letter >' in the 'before do'
# statement)
#
# # Iteration 1
# user.attribute_a = 'a_value'
# # No changes to 'user.attribute_b'
# # No changes to 'user.attribute_c'
#
# # Iteration 2
# # No changes to 'user.attribute_a'
# user.attribute_b = 'a_value'
# # No changes to 'user.attribute_c'
#
# # Iteration 3
# # No changes to 'user.attribute_a'
# # No changes to 'user.attribute_b'
# user.attribute_c = 'a_value'
# Maybe I should make something like the following but that, as well
# as the 'send' method must be used, doesn't work (the below code-line
# is just an example of what I would like to do).
#
# user.send(:"#{attr}") = 'a_value'
end
...
end
How can I improve the above code so to accomplish what I aim (I refer to the user.send(:"#{attr}") = 'a_value' part in order to set "programmatically" - that is, set a different attribute value for each iteration made - each user attribute value to 'a_value')?
You should use .send and append an = to the method name to invoke the setter, passing the value as the second argument to send:
[
:attribute_a,
:attribute_b,
:attribute_c
].each do |attr|
before do
user.send("#{attr}=", 'a_value')
end
You're effectively doing this:
user.send('attribute_a=', 'a_value');
Your syntax (user.send(:"#{attr}") = 'a_value') is wrong/weird for a couple reasons:
There's no reason to convert :attr to a string and them immediately back to a symbol
You can't assign a value to the return value of .send
Related
Can you explain pls, how's Ruby's minus operator working? Not just trivial case like 5 - 2.
A bit more complex – we have 2 arrays with ActiveRecord objects:
Array A = User.where(...), Array B = User.where(...), I wanna make A - B, how is it working? Is it just comparing objects IDs, or all attributes, or smth else?
Is it just comparing objects IDs?
Type and ID, yes.
pry(main)> show-source User#eql?
From: /Users/sergio/.gem/ruby/2.5.1/gems/activerecord-5.2.0/lib/active_record/core.rb # line 420:
Owner: ActiveRecord::Core
Visibility: public
Number of lines: 6
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
!id.nil? &&
comparison_object.id == id
end
Just for additional details from Sergio's answer:
I narrowed down what the - method (operator) does on an ActiveRecord::Relation object, because I got curious as well myself:
Rails 5:
Traceback:
# rails console (pry-rails)
users_a = User.where(...)
users_b = User.where(...)
puts users_a.class
# => `User::ActiveRecord_Relation`
show-source users_a.-
# From: /Users/jrpolidario/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.2.0/lib/active_record/relation/delegation.rb # line 41:
# Owner: ActiveRecord::Delegation
# Visibility: public
# Number of lines: 4
#
# delegate :to_xml, :encode_with, :length, :each, :uniq, :join,
# :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
# :to_sentence, :to_formatted_s, :as_json,
# :shuffle, :split, :slice, :index, :rindex, to: :records
# since the `-` method as seen above is delegated to #records, let's see what the return type is the return value of `#records` is, of which is supposed to respond to the `-` operator.
puts users_a.records.class
# => Array
# ...because it's an Array type, then let's see if the Array type responds to the delegated `-` method.
show-source users_a.records.-
# From: array.c (C Method):
# Owner: Array
# Visibility: public
# Number of lines: 17
#
# static VALUE
# rb_ary_diff(VALUE ary1, VALUE ary2)
# {
# VALUE ary3;
# VALUE hash;
# long i;
#
# hash = ary_make_hash(to_ary(ary2));
# ary3 = rb_ary_new();
#
# for (i=0; i<RARRAY_LEN(ary1); i++) {
# if (st_lookup(rb_hash_tbl_raw(hash), RARRAY_AREF(ary1, i), 0)) continue;
# rb_ary_push(ary3, rb_ary_elt(ary1, i));
# }
# ary_recycle_hash(hash);
# return ary3;
# }
... which just simply means, I quote from Array
Returns a new array that is a copy of the original array, removing any items that also appear in other_ary. The order is preserved from the original array.
Rails 4
P.S. I also tried tracing this in rails 4.2, and show-source users_a.- does not show any method, which meant that it makes use of method_missing (therefore also meant that there has been changes between 4 and 5 with regards to this), and then, tracing further, I ended up with below:
Traceback:
[127, 136] in /Users/jrpolidario/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-4.2.10/lib/active_record/relation/delegation.rb
127:
128: def method_missing(method, *args, &block)
129: if #klass.respond_to?(method)
130: scoping { #klass.public_send(method, *args, &block) }
131: elsif array_delegable?(method)
=> 132: to_a.public_send(method, *args, &block)
133: elsif arel.respond_to?(method)
134: arel.public_send(method, *args, &block)
135: else
136: super
... which as you could see .to_a is called on the ActiveRecord::Relation object which means that it becomes an array, and then method (which is '-') is called on that Array object, which also means in the end it also calls the Array#- method same in Rails 5 above.
I am trying to run a program from a book that I am learning ruby from, but I keep getting this error no matter how I try to fix it
ex41.rb:70:in `convert': wrong number of arguments (0 for 2) (ArgumentError)
from ex41.rb:117:in `block (2 levels) in <main>'
from ex41.rb:113:in `each'
from ex41.rb:113:in `block in <main>'
from ex41.rb:108:in `loop'
from ex41.rb:108:in `<main>'
I do understand that I have the wrong number of arguments, but the thing is I can't find where I'm getting them wrong, could someone please explain this to me? For convienience I put (LINE XXX(as a number)) by the lines that cause me trouble. Thank you.
# calls external file from web
require 'open-uri'
# assigns WORD_URL to website
WORD_URL = "http://learncodethehardway.org/words.txt"
# creates empty array WORDS
WORDS =[]
# creates hash PHRASES
PHRASES = {
"class ### < ### \nend" =>
"Make a class named ### that is-a ###.",
# my assumption is that ### ### and the like will sb in words, lets find
out
"class ###\n\tdef initialize(###)\n\tend\nend" =>
"class ### has-a initialize that takes ### parameters.",
"class ###\n\tdef ***(###)\n\tend\nend" =>
"class ### has-a function named *** that takes ### parameters.",
"*** = ###.new()" =>
"Set *** to an instance of class ###.",
"***.***(###)" =>
"From *** get the *** function, and call it with parameters ###",
"***.*** = '***'" =>
"From ***, get the *** attribute and set it equal to '***'."
}
# creates variable for first argument if argument is english
PHRASE_FIRST = ARGV[0] == "english"
# opens WORD_URL function and creates function f
open(WORD_URL) {|f|
# for every line in f chomps break character and pushes out word to f
f.each_line {|word| WORDS.push(word.chomp)}
}
# defines function craft_names with variables rand_words, snippet and
pattern, and assigns caps value to false
def craft_names(rand_words, snippet, pattern, caps=false)
# assigns vriable names to snippet.scan on pattern maps and does the
following
function
names = snippet.scan(pattern).map do
# assigns word to rand_words that have popped off the end
word = rand_words.pop()
# Guessing again, maybe capitalizes the first word
caps ? word.capitalize : word
# ends do
end
# returns names twice
return names * 2
# ends craft_names function
end
# defines function craft_params with variables rand_words snippet, and
pattern
def craft_params(rand_words, snippet, pattern)
# assigns names to action scan with variable pattern with action
# length on beginning part of array to snippet and runs the following
# function using map
names = (0...snippet.scan(pattern).length).map do
# assigns variable param_count to an action that takes a random 3 and adds one to it
param_count = rand(3) + 1
# assigns variable params to one long ass action that maps 0 to param_count
value and pops out the last word
params = (0...param_count).map { |x| rand_words.pop() }
# joins params list, (I'm sure its a list) with string ,
params.join(", ")
# ends names for loop
end
# returns names twice
return names * 2
# ends craft_params function
end
# defines convert function with variables snippet and phrase
(LINE 70) def convert(snippet, phrase)
# sords words randomly and assigns words to rand_words
rand_words = WORDS.sort_by {rand}
# assigns class names to function craft mnames on rand_words, snippet /###/, and caps=true
class_names = craft_names(rand_words, snippet, /###/, caps=true)
# assigns other_names to craft_names with variables rand_words, snippet, and /\*\*\*/
other_names = craft_names(rand_words, snippet, /\*\*\*/)
# assigns param_names to craft_params with rand_words, snippet, and ### as variables
param_names = craft_params(rand_words, snippet, /###/)
# assigns empty array results
results = []
# on all variables snippet and phrase matchups perform the function
sentence
[snippet, phrase].each do |sentence|
# fake class names, also copies sentence
result = sentence.gsub(/###/) {|x| param_names.pop}
# fake other names
result.gsub!(/\*\*\*/)
# fake parameter lists
result.gsub!(/###/)
# returns result
results.push(result)
# ends function
end
# returns results
return results
# ends convert function
end
# keep going until they hit ctrl-d
# continual loop
(LINE 108)loop do
# assigns variable snippets to Prases.keys and sorts randomly
snippets = PHRASES.keys().sort_by {rand}
# for snippet in snippets
(LINE113) snippets.each do |snippet|
# assigns PHRASES on snippet to phrase
phrase = PHRASES[snippet]
# asssigns question and answer to converted snippet and phrase
#respectively
(LINE117)question, answer = convert[snippet, phrase]
# if values in phrase firs are equal to answer, question
if PHRASE_FIRST
# question, answer = answer, question
question, answer = answer, question
# ends if
end
# prints question, two line breaks, and > without line break
print question, "\n\n> "
# closes program unless input
exit(0) unless $stdin.gets
# prints line break ANSWER: answer line break line break
puts "\nANSWER: %s\n\n"
# ends unless
end
# ends loop
end
TL;DR – Use convert(snippet, phrase) instead of convert[snippet, phrase]
When you write
convert[snippet, phrase]
... it is equivalent to:
convert()[snippet, phrase]
Adding a space:
convert [snippet, phrase]
... would call the method with an array:
convert([snippet, phrase])
To actually call the method with two arguments, use:
convert(snippet, phrase)
or
convert snippet, phrase
In one of my helpers I have to differ if the user is on a profile or not. The helper returns an array full of URLs.
def get_options(profile)
if profile
url_arr = profile_infinite_path(user, ...)
# .
# .
# do stuff with profile_infinite_path
else
url_arr = infinite_path(user, ...)
# .
# .
# do same stuff with infinite_path
end
end
I want to make this code more DRY, so my plan was to store the path as a variable and then simply calling all the remaining code only once.
def get_options(profile)
if profile
var_path = profile_infinite_path
else
var_path = infinite_path
end
url_arr = var_path(user, ...)
# .
# .
# do stuff with var_path
end
I also tried storing the path as a method, but no luck there.
var_path = profile_infinite_path.method
You have two options here. Since path helpers are methods, and methods can be invoked without arguments simply by stating them, an assignment like path = profile_inifite_path gives the result of the method invocation.
You can defer invocation by using a symbol to refer to the method, and then sending it as a message when needed:
var_path = :profile_infinite_path
# ...
send(var_path, user, ...)
The symbol is the first argument to send, and it is followed by any arguments you would have given to the method.
The other way you could handle this is to wrap the method call in a proc and invoke it when needed:
var_path = ->(*args){ profile_infinite_path(*args) }
# ...
var_path.call(user, ...)
I tend to prefer send for situations like this.
Store only a symbol in your variable eg
var_path = :profile_infinite_path
Then you can do send(var_path, other_args) to get the real URL.
Eg if you have users:
var_path = :user_path
send(var_path, 2) would return "/users/2"
In ruby you can assign the results of an if-else expression to a variable. This allows you to call the desired method and assign the results like so:
url_arr = if profile
profile_infinite_path(user, ...)
else
infinite_path(user, ...)
end
# .
# .
# do stuff with url_arr
I have a set of API keys that I want to load into the rails environment, for easy and frequent access (without hitting the database). How can I lazy load it:
1) Let's say there is an API_KEY hash in rails environment (that I initialize using an initializer)
2) When I look up for a key, I first look up in API_KEY hash, if not found, I fetch from database, and at the same time add to API_KEY hash, such that it is accessible for all future requests.
3) In case someone changes the the api key, I can update API_KEY hash in case the key exists.
Can someone help me with the calls to create, delete and update the API_KEY hash in the rails environment (from within the code, instead of the initial rails loading)? Will there be a problem in this approach if each passenger thread loads the rails environment separately?
Any other problems that you see with this approach? The data set (number of api keys) is finite.
Since you're actually just storing values in a Hash (its being assigned to a constant is immaterial since you're not freezing it or anything) I would use the block form of Hash.new. I don't know what your database looks like, but supposing you had these values stored in a model called APIKey that has attributes name and value:
API_KEY = Hash.new do |hash, key_name|
hash[key_name] = APIKey.where(:name => key_name).pluck(:value)
end
Now when you access the API_KEY hash with a key that doesn't exist, it will query the APIKey model and assign the value of the value attribute to that element of the hash. Suppose you have this in your api_keys table:
name value
--------- ----------------
S3_ACCESS 0123456789abcdef
Then you could access the hash defined above like this:
puts API_KEY[:S3_ACCESS]
# Query: SELECT `value` FROM `api_keys` WHERE `name` = 'S3_ACCESS']
# => 0123456789abcdef
puts API_KEY.inspect
# => { :S3_ACCESS => "0123456789abcdef" }
puts API_KEY[:S3_ACCESS]
# No query!
# => 0123456789abcdef
If you want to update the value at runtime then you can do it like any hash:
API_KEY[:S3_ACCESS] = '5555555555ffffff'
# => "5555555555ffffff"
puts API_KEY.inspect
# => { :S3_ACCESS => "5555555555ffffff" }
However, changing the hash will not update the database record.
Advanced
If you want the database record to be updated if you update the hash, you'll have to override Hash#[]=, and if you're going to go that far you might as well use ActiveRecord directly. For example:
class APIKey < ActiveRecord::Base
attr_accessible :name, :value
##_cached_values = Hash.new do |_cached_values, key_name|
# This will return nil if there's no record in the database with the
# given name; alternatively you could use `first!` which would raise a
# RecordNotFound exception which you could rescue and do something
# useful with.
_cached_values[key_name] = self.where(:name => key_name).pluck(:value)
end
def self.[](key_name)
##_cached_values[key_name]
end
def self.[]=(key_name, new_value)
# If the database already has a value for this key_name, fetch the object;
# otherwise initialize a new object
api_key = self.where(:name => key_name).first_or_initialize
# Update the value and save the record
api_key.update_attributes!(:value => new_value)
# Update the cached value
##_cached_values[key_name] = new_value
end
end
puts APIKey[:S3_ACCESS]
# Query: SELECT `value` FROM `api_keys` WHERE `name` = 'S3_ACCESS'
# => 0123456789abcdef
APIKey[:S3_ACCESS] = '5555555555ffffff'
# Query: UPDATE `api_keys` SET `value` = '5555555555ffffff'
# WHERE `name` = 'S3_ACCESS'
# => '5555555555ffffff'
APIKey[:NEW_KEY] = 'new_val'
# Query: INSERT INTO `api_keys` (`name`, `value`)
# VALUES ('NEW_KEY', 'new_val')
# => 'new_val'
With this kind of implementation APIKey[:S3_ACCESS] would work the same as the API_KEY example above; and APIKey[:S3_ACCESS] = 'foo' would perform an UPDATE or INSERT as necessary.
This is probably reinventing the wheel, though; at this point you're probably better off using one of the many configuration management gems people much smarter than me have written. Also, let us not forget The Twelve-Factor App, which exhorts us to store config in the environment.
P.S. You get ridiculous overimplementation points if you define APIKey.const_missing so you can do APIKey::S3_ACCESS instead of APIKey[:S3_ACCESS].
Student.find(:all, :conditions => [‘name = ? and status = ?’ ‘mohit’, 1])
Vs
Student.find_all_by_name_and_status(‘mohit’, 1)
Both the queries will result the same set of row but first one is preferable cause in the second way there will be exception generated method_missing and then rails will try to relate it as dynamic method. if fine then result set to returned.
Can any body explain me this in a good manner. What exactly is happening behind the screen. Please correct me if i am wrong.
You are right, the second way will go through a method_missing. ActiveRecord will parse the method name and if it is a valid name, it will generate a method on the fly.
If you look in the source of ActiveRecord::Base, in method_missing you'll see that developers left us a comment of how this generated method would look like:
# def self.find_by_login_and_activated(*args)
# options = args.extract_options!
# attributes = construct_attributes_from_arguments(
# [:login,:activated],
# args
# )
# finder_options = { :conditions => attributes }
# validate_find_options(options)
# set_readonly_option!(options)
#
# if options[:conditions]
# with_scope(:find => finder_options) do
# find(:first, options)
# end
# else
# find(:first, options.merge(finder_options))
# end
# end
So you see that generally it boils down to the same find method.
I would not say that the first way is preferable because of method_missing, because the performance penalty for that is negligible. The second way reads better and works well if you just need to fetch records based on attributes equal to some values.
However, this second form does not allow you to do anything beyond equality comparison (e.g., range comparison, "not equal to" expressions, joins, etc.). In such cases, you'll just have to use the find method with appropriate conditions and other parameters.