ruby-1.9.3-p194
Rails 3.0.9
I encountered so much strange behavior of conditional expression evaluation.
Look at piece of code:
module SimpleCaptcha
module ControllerHelpers
def simple_captcha_valid?
t = Logger.new(STDOUT)
return true if Rails.env.test?
if params[:captcha]
data = 'SHGHGD'
result = data == params[:captcha].delete(" ").upcase
t.debug data
t.debug params[:captcha].delete(" ").upcase
t.debug result
else
return false
end
end
end
end
Here is what I see in debug console:
SHGHGD
WEWE
nil
As you can see nil is a result of evaluation result = data == params[:captcha].delete(" ").upcase
But why???
data is 'SHGHGD'
params[:captcha].delete(" ").upcase is WEWE
Why nil? it must be false.
This is actually because of the logger - Logger.debug false outputs nil. To understand why you need to look into the logger class (logger.rb). The debug, info, warn etc. methods all end up calling
def add(severity, message = nil, progname = nil, &block)
severity ||= UNKNOWN
if #logdev.nil? or severity < #level
return true
end
progname ||= #progname
if message.nil?
if block_given?
message = yield
else
message = progname
progname = #progname
end
end
end
and message will be nil, progname will be the value you've passed (ie a string). The keyline is progname ||= #progname. Because progname is false, it is overwritten with the value of #progname, which is nil so that is what gets output.
Related
I have a service method that makes api requests and if the response was not ok, it would notify Bugsnag. It looks like this:
def send_request
#response = HTTParty.get(api_endpoint, options)
return JSON.parse(#response.body, symbolize_names: true) if #response.ok?
raise StandardError.new(JSON.parse(#response.body))
rescue StandardError => exception
BugsnagService.notify(exception, #response)
end
My BugsnagService#notify looks something like this:
class BugsnagService
def self.notify(exception, response = nil, **options)
if response
response_body = if valid_json?(response.body) # Error right here
JSON.parse(response.body)
else
response.body
end
options[:response_body] = response_body
options[:response_code] = response.code
end
# Raising exception in test and development environment, or else the exception will be
# silently ignored.
raise exception if Rails.env.test? || Rails.env.development?
Bugsnag.notify(exception) do |report|
report.add_tab(:debug_info, options) if options.present?
end
end
def self.valid_json?(json_string)
JSON.parse(json_string)
true
rescue JSON::ParserError => e
false
end
end
I set response = nil in my notify method because not every error is an API error, so sometimes I would just call BugsnagService.notify(exception).
I found out that if I just call it like I am in the snippet above, it would raise an error saying it can't call .body on a Hash. Somehow, when I pass #response into BugsnagService#notify, the object turns from HTTParty::Response into Hash.
But if I pass something in for the **options parameter, it will work. So I can call it like this:
BugsnagService.notify(exception, #response, { })
I've been trying to figure this one out but I couldn't find anything that would explain this. I'm not sure if there's something wrong with the way I define my parameters or if this is some bug with the HTTParty gem. Can anyone see why this is happening? Thanks!
The problem is that your #response is being passed in as the options, as response can be nil. The double splat is converting it to a hash.
Try:
def testing(x, y = nil, **z)
puts "x = #{x}"
puts "y = #{y}"
puts "z = #{z}"
end
testing 1, 2, z: 3
#=> x = 1
#=> y = 2
#=> z = {:z=>3}
testing 1, y: 2
#=> x = 1
#=> y =
#=> z = {:y=>2}
testing 1, { y: 2 }, {}
#=> x = 1
#=> {:y=>2}
#=> {}
I'd suggest the best approach would be to have response be a keyword arg, as in:
def self.notify(exception, response: nil, **options)
...
end
That way, you can still omit or include the response as desired, and pass in subsequent options.
What I'm thinking of is something where I can say:
e = Foo.new
e.bar = "hello"
e.save
e.reload
e.bar.nil!
e.reload
e.bar.nil? => true
Kind of #touch but sets nil and saves.
EDIT
Super sorry guys. I mean this:
e = Foo.new
e.bar = "hello"
e.save
e.reload
e.bar.nil!
e.reload
e.bar.nil? => true
Maybe something like:
module ActiveRecord
class Base
def nil!(*names)
unless persisted?
raise ActiveRecordError, <<-MSG.squish
cannot nil on a new or destroyed record object. Consider using
persisted?, new_record?, or destroyed? before nilling
MSG
end
unless names.empty?
changes = {}
names.each do |column|
column = column.to_s
changes[column] = write_attribute(column, nil)
end
primary_key = self.class.primary_key
scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key))
if locking_enabled?
locking_column = self.class.locking_column
scope = scope.where(locking_column => _read_attribute(locking_column))
changes[locking_column] = increment_lock
end
clear_attribute_changes(changes.keys)
result = scope.update_all(changes) == 1
if !result && locking_enabled?
raise ActiveRecord::StaleObjectError.new(self, "nil")
end
#_trigger_update_callback = result
result
else
true
end
end
end
end
Put that in an initializer and it'll let you null out the title of a comment with Comment.last.nil!(:title).
You can't save a nil to the database, and furthermore, once an object has been created as a particular class you can never change that. It can only be converted by creating a new object, something an in-place modifier like this hypothetical nil! does.
The closest thing you can get is:
e = Foo.new
e.bar = "hello"
e.save
e.reload
e.delete!
e.reload
e.destroyed? # => true
f = Foo.find_by(id: e.id)
f.nil? # => true
In Ruby 2.1.5 and 2.2.4, creating a new Collector returns the correct result.
require 'ostruct'
module ResourceResponses
class Collector < OpenStruct
def initialize
super
#table = Hash.new {|h,k| h[k] = Response.new }
end
end
class Response
attr_reader :publish_formats, :publish_block, :blocks, :block_order
def initialize
#publish_formats = []
#blocks = {}
#block_order = []
end
end
end
> Collector.new
=> #<ResourceResponses::Collector>
Collector.new.responses
=> #<ResourceResponses::Response:0x007fb3f409ae98 #block_order=[], #blocks= {}, #publish_formats=[]>
When I upgrade to Ruby 2.3.1, it starts returning back nil instead.
> Collector.new
=> #<ResourceResponses::Collector>
> Collector.new.responses
=> nil
I've done a lot of reading around how OpenStruct is now 10x faster in 2.3 but I'm not seeing what change was made that would break the relationship between Collector and Response. Any help is very appreciated. Rails is at version 4.2.7.1.
Let's have a look at the implementation of method_missing in the current implementation:
def method_missing(mid, *args) # :nodoc:
len = args.length
if mname = mid[/.*(?==\z)/m]
if len != 1
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
end
modifiable?[new_ostruct_member!(mname)] = args[0]
elsif len == 0
if #table.key?(mid)
new_ostruct_member!(mid) unless frozen?
#table[mid]
end
else
err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
err.set_backtrace caller(1)
raise err
end
end
The interesting part is the block in the middle that runs when the method name didn't end with an = and when there are no addition arguments:
if #table.key?(mid)
new_ostruct_member!(mid) unless frozen?
#table[mid]
end
As you can see the implementation first checks if the key exists, before actually reading the value.
This breaks your implementation with the hash that returns a new Response.new when a key/value is not set. Because just calling key? doesn't trigger the setting of the default value:
hash = Hash.new { |h,k| h[k] = :bar }
hash.has_key?(:foo)
#=> false
hash
#=> {}
hash[:foo]
#=> :bar
hash
#=> { :foo => :bar }
Ruby 2.2 didn't have this optimization. It just returned #table[mid] without checking #table.key? first.
I have a method that contains the following code.
def save_question(content)
question = Question.new
question.content = content
question.save
end
When I run this in an if statement
if save_question(content)
puts "Everything is cool"
else
puts "Something went wrong"
end
The method returns "Everything is cool". However if I change the method to this
def save_question(content)
question = Question.new
question.content = content
return false unless question.save
end
Then the if statement will return "Something went wrong". Am I missing something big here? I thought the save method returns true, which is does, but why does the method return false?
You're modifying your method so that it returns false or nil, which is also falsy.
Your last line now reads
return false unless question.save
There is no implicit return true here. If question.save returns true, the return false is never executed, and the expression evaluates to nil.
Think of it this way: What would you expect this version of the function to return?
def save_question(content)
if !question.save
return false
end
end
I've a setter method for an attr_accessor in rails
# setter method of the shopify_p accessor
def shopify_v=(s)
begin
self.product.shop.connect_to_store
#shopify_v = s if s.save
ensure
ShopifyAPI::Base.site = nil
end
end
I'd like it to return true if save is successful or false if the save action doesn't work.
Instead it always outputs the s object (or #shopify_v, I don't know).
How can I make it return true or false depending on the save action?
Thanks,
Augusto
UPDATE #1
Here is the getter method of the same attr_accessor.
Basically it downloads the object from the server only in case it has never done it before.
# getter method of the shopify_v accessor
def shopify_v
if #shopify_v.nil?
begin
self.product.shop.connect_to_store
#shopify_v = ShopifyAPI::Variant.find(self.shopify_id)
ensure
ShopifyAPI::Base.site = nil
end
puts "remote"
return #shopify_v
else
puts "local"
return #shopify_v
end
end
def shopify_v=(s)
begin
self.product.shop.connect_to_store
#shopify_v = s if s.save
ensure
ShopifyAPI::Base.site = nil
end
#shopify_v.present? # will return true or false
end
I would use exceptions, since being unable to save is an exceptional condition. Also, whenever practical, a method should either have a side-effect, or return a value.
For example:
class ShopifyError < StandardError ; end
def shopify_v=(s)
begin
self.product.shop.connect_to_store
raise ShopifyError unless s.save
#shopify_v = s
ensure
ShopifyAPI::Base.site = nil
end
end
and in the caller:
begin
...
model.v = s
...
rescue ShopifyError
# set flash[:notify], or whatever error handling is appropriate
end
Also, there are cleaner ways to structure the getter. Consider doing something like this:
# getter method of the shopify_v accessor
def shopify_v
#shopify_v ||= fetch_v
end
private
def fetch_v
begin
self.product.shop.connect_to_store
ShopifyAPI::Variant.find(self.shopify_id)
ensure
ShopifyAPI::Base.site = nil
end
end
A setter always returns the value being set, no matter what you try to return. So you should use another method name, for example:
def set_shopify_v(s)
self.product.shop.connect_to_store
status = s.save
#shopify_v = s if status
status
rescue => exc
# Rails.logger.error(...)
false
ensure
ShopifyAPI::Base.site = nil
end