Why Rails can use `if` as hash key but not in Ruby - ruby-on-rails

In pure Ruby irb, one cannot type {if: 1}. The statement will not terminate, because irb thinks if is not a symbol but instead the beginning of an if statement.
So why can Rails have before_filter which accept if as parameters? The guide have codes like:
class Order < ApplicationRecord
before_save :normalize_card_number, if: :paid_with_card?
end
Same thing happens to unless as well.

That's an irb issue, not Ruby.
bash=> ruby -e "puts({if: 1})"
bash=# {:if=>1}
You can use pry instead. It will read input correctly.
https://github.com/pry/pry

IRb's parser is well-known to be broken. (In fact, the very bug you encountered was already reported months ago: Bug #12177: Using if: as symbol in hash with new hash syntax in irb console is not working.) Just ignore it. There are also other differences in behavior between IRb and Ruby, semantic ones, not just syntactic. E.g. methods defined at the top-level are implicitly public instead of implicitly private as they should be.
IRb tries to parse the code with its own parser to figure out, e.g. whether to submit it to the engine when you hit ENTER or wait for you on the next line to continue the code. However, because Ruby's syntax is extremely complex, it is very hard to parse it correctly, and IRb's parser is known to deviate from Ruby's.
Other REPLs take different approaches, e.g. Pry actually uses Ruby's parser instead of its own.

The code in your example is part of the Rails DSL. What you are actually setting there is a hash which just happens to look a bit like code.
Internally, Rails will evaluate this hash specifying conditions to the before_save call.
In a very simplified version, Rails basically does this when saving:
class ActiveRecord::Base
#before_save_rules = []
def self.before_save(method, options={})
#before_save_rules << [method, options]
end
def self.before_save_rules
#before_save_rules
end
def save
# Evaluate the defined rules and decide if we should perform the
# before_save action or not
self.class.before_safe_rules.each do |method, options|
do_perform = true
if options.key?(:if)
do_perform = false unless send(options[:if])
end
if options.key?(:unless)
do_perform = false if send(options[:unless])
end
send(method) if do_perform
end
# now perform the actual save to the database
# ...
end
end
Again, this is very simplified and just in the spirit of actual code, but this is basically how it works.

Related

Rails 3.2.21 and Ruby 2.2.0 breaking Time.zone.parse

With Rails 3.2.21 and Ruby 2.2.0p0 the time zone parser is broken. With Ruby 2.1.2 this was working just fine.
[1] pry(main)> Time.zone.parse("2015-01-12")
NoMethodError: undefined method `year' for nil:NilClass
from /Users/user/.rvm/gems/ruby-2.2.0/gems/activesupport-3.2.21/lib/active_support/values/time_zone.rb:275:in `parse'
Now I know that you can replace it with Time.parse("2015-01-12").localtime but this breaks functionality in my apps. Are there any known fixes for this?
TLDR: rails bug that has been fixed, first fixed version on the 3.2 branch is 3.2.22
Ruby 2.2 changes how default arguments are resolved when there is a name ambiguity:
def now
...
end
def foo(now = now)
end
In older versions of ruby calling foo with no arguments results in the argument now Being set to whatever the now() method calls. In ruby 2.2 it would instead be set to nil (and you get a warning about a circular reference)
You can resolve the ambiguity by doing either
def foo(now = now())
end
Or
def foo(something = now)
end
(And obviously changing uses of that argument)
Apparently the way it used to work was a bug all along. Rails had a few places where this bad behaviour was relied on, including in AS::Timezone.parse. The fix was backported to the 3-2-stable branch and eventually released as part of 3.2.22.
The commit to rails master fixing the issue has a link to the ruby bug filed about this
So, this was your original situation:
class Dog
def do_stuff(x, y=2)
puts x + y
end
end
d = Dog.new
d.do_stuff(1)
--output:--
3
But, the code for do_stuff() has changed, and now you are faced with something similar to this:
class Dog
def do_stuff(x, y=nil)
puts x + y
end
end
d = Dog.new
d.do_stuff(1)
--output:--
1.rb:4:in `+': nil can't be coerced into Fixnum (TypeError)
from 1.rb:4:in `do_stuff'
from 1.rb:10:in `<main>'
Blasted !##$!##$!##$!##$!#### developers!!!
In ruby, you can use alias_method() to create an additional name for a method:
class Dog #Reopen the previously defined Dog class.
alias_method :orig_do_stuff, :do_stuff #Create additional name for do_stuff()
#Now redefine do_stuff():
def do_stuff(x, y=2) #Use a better default value for y.
orig_do_stuff(x, y) #Call the original method.
end
end
d.do_stuff(1)
--output:--
3
According to the rails docs, Time.zone() returns a TimeZone object, so that's the class that defines parse(), which is the method you want to alias. So, the code would look like this:
class Timezone #Re-open the Timezone class.
alias_method :orig_parse, :parse #Create an additional name for parse().
def parse(str, now=now()) #Now, redefine parse().
orig_parse(str, now) #Call the original parse() method.
end
end
Then, you can call parse() like you always have:
Time.zone.parse("2015-01-12")
I guess you should put the code inside app/helpers/application_helper.rb. See if that works.
I think the above would be considered an Adapter pattern. Although, because the plug already fits--it just does't produce the results you want--it might be considered a Decorator pattern. So, now you can put on your resume that you use custom Adapter and/or Decorator patterns in your rails code. :)
Extending 7stud's answer, since parse method is defined in ActiveSupport::TimeZone class, you can simply open it and make this slight change accordingly.
You may place this under config/initializers/timezone.rb or refer this question for alternative places to pace this.
class ActiveSupport::TimeZone
alias_method :orig_parse, :parse #Create an additional name for parse().
def parse(str, now=now()) #Now, redefine parse().
orig_parse(str, now) #Call the original parse() method.
end
end

Ruby on Rails decorator for caching a result?

In Python, you can write a decorator for memoizing a function's response.
Is there something similar for Ruby on Rails? I have a model's method that makes a query, which I would like to cache.
I know I can do something inside the method, like:
def foo(param)
if self.cache[param].nil?
self.cache[param] = self.get_query_result(param)
else
self.cache[param]
end
end
However, given that I would do this often, I'd prefer a decorator syntax. It is clearer and better IMO.
Is there something like this for Ruby on Rails?
I usually do this using custom accessors, instance variables, and the ||= operator:
def foo
#foo ||= something_or_other
end
something_or_other could be a private method on the same class that returns the object that foo should be.
EDIT
Here's a slightly more complicated solution that lets you cache any method based on the arguments used to call them.
class MyClass
attr_reader :cache
def initialize
#cache = {}
end
class << self
def cacheable(symbol)
alias_method :"_#{symbol}_uncached", symbol
define_method(symbol) do |*args|
cache[[symbol, *args]] ||= (send :"_#{symbol}_uncached", *args)
end
end
end
end
How this works:
class MyClass
def foo(a, b)
a + b
end
cacheable :foo
end
First, the method is defined normally. Then the class method cacheable is called, which aliases the original method to a new name, then redefines it under the original name to be executed only if it's not already cached. It first checks the cache for anything using the same method and arguments, returns the value if present, and executes the original method if not.
http://martinfowler.com/bliki/TwoHardThings.html:
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
Rails has a lot of built in caching(including query caching). You might not need to do anything:
http://guides.rubyonrails.org/caching_with_rails.html
Here is a recent blog post about problems with roll your own caching:
http://cmme.org/tdumitrescu/blog/2014/01/careful-what-you-memoize/

How do I make to_json work in Rails as it works in plain Ruby?

This issue will surface for many who depend on Ruby's JSON serialization outside of a Rails projects. When they try to use their code in a Rails project, it will not work as expected.
The following code run from Ruby (no Rails), prints A.
When run from rails console, it prints Hash.
That means my json serialization works in my command line lib/app, but not when it's imported into a Rails project.
What is the reason/workaround for this?
require 'json'
class A
def to_json(*a)
{:json_class => self.class.name}.to_json(*a)
end
def self.json_create(o)
A.new
end
end
class B
attr_accessor :value
def initialize(value)
#value = value
end
def to_json(*a)
{:json_class => self.class.name, :value => value}.to_json(*a)
end
def self.json_create(o)
B.new(o['value'])
end
end
b = JSON.parse(B.new(A.new).to_json)
puts b.value.class
Ruby is 1.9.3, Rails is 3.2.10
The problem is that Rails uses ActiveSupport::JSON.
For serializing, it uses as_json, not to_json. So the line
{:json_class => self.class.name, :value => value}.to_json(*a)
does not include a JSON version of value in the hash because Class A does not have a as_json method. To get your code to work the same in both Ruby and Rails, you need to explicitly call your A::to_json and A::json_create methods, like this:
def to_json(*a)
{:json_class => self.class.name, :value => JSON.dump(value)}.to_json(*a)
end
def self.json_create(o)
B.new(A.json_create(o['value']))
end
Then call, b = JSON.parse(JSON.dump(B.new(A.new)))
This wlll fix the example, but I think you may want to read this explanation of to_json vs as_json and revise your code appropriately.
According to others, the answer is yes.
http://www.rubyhood.com/2011/06/rails-spoiled-standard-json-library.html
In short, make as_json do what to_json does. That got me what I wanted/expected (and what I've been getting from pure Ruby - Rails).
For those still wandering why the strange behavior is occurring in rails the explanation can be found in:
https://github.com/flori/json/compare/v1.6.7...v1.6.8
and
https://github.com/intridea/multi_json/compare/v1.5.0...v1.5.1
Since in these version upgrades JSON.parse works different. JSON.load might still be helpful. The fastest fix would be:
gem 'json', '1.6.7'
gem 'multi_json', '1.5.0'
but leave some security issues open. Explicitly supplying create_additions: true to JSON parse when needed is recommended.

Elegant way to stringify keys with Rails and Mongo?

If you supply Mongo with a hash that uses symbols as keys and save the document, it will 'stringify' it, meaning the keys will be converted to strings. To summarize:
condition: hash keys will be:
---------- ------------------
after initializing a document symbols or strings
after saving a document strings
after fetching a document strings
This 'asymmetry' has led to some ugliness in my tests. I would like to be able to 'rely on' the keys always being strings - and not worry about if the document has just been initialized or not.
What are one or more elegant ways to avoid this?
Note: In my case, I'm using Mongoid, but I don't think this question is necessarily Mongoid specific. It probably applies to any Rails project that uses MongoDB.
Something along these lines could work. Basically this code redefines Mongoid's field macro (its setter).
require 'mongoid'
module Stringifier
def field name, args = {}
super # call mongoid implementation
define_method "#{name}=" do |val|
val.stringify_keys! if val && val.respond_to?(:stringify_keys!)
super(val)
end
end
end
class Foo
include Mongoid::Document
extend Stringifier
field :subhash, type: Hash
end
f = Foo.new
f.subhash = {a: 1, b: 2}
puts f.subhash
# >> {"a"=>1, "b"=>2}
This may not be the cleanest implementation, but you get the idea.
My current solution is to override each field setter to call stringify_keys!. For example:
def field_name=(x)
x.stringify_keys! if x
super(x)
end
This it the best I've found so far. I considered other alternatives:
Using a before_validation callback. However, I don't like this approach. I didn't like having to call valid? in order to trigger stringification.
Using after_initialize. However, this doesn't handle the case of calling a setter after initialization.

Ruby on Rails 2.3.8: How do I set up my unit tests such that when there is an error, I can have all the variables in the current scope printed?

Let's say I'm in a for loop, and something in the for loop breaks.
How would I set up some sort of exception catching so that when my testing fails, all variables visible to the current scope, including loop index, are printed to the console?
Maybe there is a way to change the method that ruby uses to output all the error text to the console? and include all the variable currently in scope?
Although you can iterate over ##class and #instance variables, as far as I know the regular Ruby interpreters do not allow you to show all local, scoped variables. These are really just constructs in the parse tree and are not preserved when they fall out of scope. Further, you may have multiple local variables with the same name, but only the top-most one will be visible at any given time.
You might have to define your own rescue handler that dumps out any variables you've tagged as being interested in. Either that or experiment with using the Ruby debugger.
An alternative is something like this:
def clear_capture!
#captured = nil
end
def capture(*args)
(#captured ||= { }).merge(args)
end
def dump_capture
puts #capture.inspect
end
In your tests you'd do something like this:
def setup
clear_captured!
end
def test_something
foo = :foo
bar = 12
capture(:foo => foo, :bar => bar)
# ...
capture(:foo => foo, :other => other)
rescue
report_capture
end
You could enhance this to use eval which would make the syntax tighter.

Resources