Using union types with flow sensitivity - ruby-on-rails

I'm trying to use union types with is_a? for flow control, but I'm still getting sorbet errors. I've tried casting as well, and I'm still running into the same error, which is:
Method to_hash does not exist on T::Array[T.untyped] component of T.any(T::Array[T.untyped], T::Hash[Symbol, T.untyped]) https://srb.help/7003
I have the following struct:
class PostProcessingMethod < T::Struct
prop :method_name, Symbol
prop :args, T.any(Array, T::Hash[Symbol, T.untyped]), default: []
prop :changed_fields, T::Array[String], default: []
prop :all, T::Boolean, default: false
prop :force, T::Boolean, default: false
end
and I'm using it in a method that (currently) looks like this:
sig { params(post_processing_methods: T::Array[Documents::PostProcessingMethod]).void }
def call(post_processing_methods)
post_processing_methods.each do |post_processing_method|
next unless should_call_method?(post_processing_method)
if #object.respond_to?(post_processing_method.method_name)
if post_processing_method.args.is_a?(Array)
#object.send(post_processing_method.method_name, *post_processing_method.args)
elsif post_processing_method.args.is_a?(Hash)
#object.send(post_processing_method.method_name, **post_processing_method.args)
end
end
end
end
I've tried incorporating T.cast to ensure that sorbet knows it's a Hash in the elsif, but that doesn't seem to have made a difference.
My expectation is that is_a? should allow sorbet to know that post_processing_method is a Hash in the elseif. But if that's not the case, the T.cast should certainly handle this.

In the code posted, each .args call is treated like a new variable. If you capture the returning value in a local variable, the flow sensitivity will work.
See an example on sorbet.run

Related

Evaluating list of boolean methods ruby

I have a list of methods who if any of them evaluated to true, I have to trigger an action on the model, in this case it's model audit.
So for example :
def a?
false # in reality this is some code
end
def b?
true # in reality this is some code
end
def c?
true # in reality this is some code
end
Now I can group this into like a parent method like :
def parent_method
a? || b? || c?
end
This will short-circuit the code, and c? will never be executed which is great. I can execute my .audit method.
But if I wanted to pass in custom message to my .audit method, and I wanted to have different message for every method how can I do that?
My first though to have a hash with keys being the methods and values being the messages or something along that line. But in that case the short circuiting doesn't work as all methods are evaluated in advance. How can I make this work better and more efficient/elegant?
Instead of true your method could return a trueish value like a symbol.
def a?
false # in reality this is some code
end
def b?
:b # in reality this is some code
end
def c?
:c # in reality this is some code
end
That you still allow to short-circuit the code in
def parent_method
a? || b? || c?
end
But now parent_method will not only return true or false but it would return a symbol that your allow returning a message that might be stored in a hash:
key = parent_method
audit(key) if key
You can always break these out into a simple array if you're not passing in any arguments. A minimal example looks like:
TESTS = [ :a?, :b?, :c? ]
def parent_method
failed = TESTS.find do |test|
!send(test)
end
if (failed)
# The failed variable contains the name of the method that failed
# to return `true`. Do whatever you need to do here.
errors << "Test #{failed} returned false!"
false
else
true
end
end
if I wanted to pass in custom message to my .audit method, and I wanted to have different message for every method how can I do that?
You could use a case expression:
case
when a? then audit(:a)
when b? then audit(:b)
when c? then audit(:c)
end

Validate params based on condition in rails-grape

I am developing APIs for a web app. In one specific case I am facing a situation where I require param2 only when value of param1 is false. I was able to use conditional statements to achieve it. I would like to know is there any inbuilt methods in grape for this type of validation. Something on the lines below
params do
requires :model_name, type: Hash do
optional :params1
if params1 == false
require :params2
end
end
end
You probably need mutually_exclusive validation that ensures given params aren't present at the same time in a request
params do
requires :model_name, type: Hash do
optional :params1
optional :params2
mutually_exclusive :params1, :params2
end
end
Grape supports it starting from version 0.8.0
There are also other options like exactly_one_of, all_or_none_of, at_least_one_of. Read more
Also for more complex conditions, you can express this relationship through the given method
given params1: -> (params1) { params1 == false } do
require :params2
end

How to call local variable value in a method through a parameter?

In my Rails app I have this (rather silly) method:
def my_method(param)
foo = "hey"
bar = "ho"
if param == :foo
return foo
elsif param == :bar
return bar
end
end
I don't like the if/else block, though.
Is there a simpler way to return the value of the local variable foo if :foo is provided as a parameter?
Or will I have to use an array or a hash here?
If you're using the very latest Ruby, you can use binding.local_variable_get(param). A hash seems cleaner to me, but your mileage may vary.
This should look simpler, don't think introducing a new data structure is required:
def my_method(param)
return 'hey' if param == :foo
return 'ho' if param == :bar
end
You can use a Hash:
def my_method(param)
objs = {
foo: "hey",
bar: "ho"
}
objs[param]
end
This is really a good time to use a case statement:
def my_method(param)
case param
when :foo
'hey'
when :bar
'ho'
else
# what do you want to do here?
end
end
Something to consider is, you're using an if/elseif, but what happens if neither of those hit? Do you want to return nil, or trap an error? As you look around in other people's code, you'll sometimes find long chains of if/elseif tests, with no final else, which opens up a potential logic error and can result in a hard-to-find bug.

Ruby syntax: break out from 'each.. do..' block

I am developing a Ruby on Rails app. My question is more about Ruby syntax.
I have a model class with a class method self.check:
class Cars < ActiveRecord::Base
...
def self.check(name)
self.all.each do |car|
#if result is true, break out from the each block, and return the car how to...
result = SOME_CONDITION_MEET?(car) #not related with database
end
puts "outside the each block."
end
end
I would like to stop/break out from the each block once the result is true (that's break the each block if car.name is the same as the name parameter once) AND return the car which cause the true result. How to break out in Ruby code?
You can break with the break keyword. For example
[1,2,3].each do |i|
puts i
break
end
will output 1. Or if you want to directly return the value, use return.
Since you updated the question, here the code:
class Car < ActiveRecord::Base
# …
def self.check(name)
self.all.each do |car|
return car if some_condition_met?(car)
end
puts "outside the each block."
end
end
Though you can also use Array#detect or Array#any? for that purpose.
I provide a bad sample code. I am not directly find or check something
from database. I just need a way to break out from the "each" block if
some condition meets once and return that 'car' which cause the true
result.
Then what you need is:
def check(cars, car_name)
cars.detect { |car| car.name == car_name }
end
If you wanted just to know if there was any car with that name then you'd use Enumerable#any?. As a rule of thumb, use Enumerable#each only to do side effects, not perform logic.
you can use include? method.
def self.check(name)
cars.include? name
end
include? returns true if name is present in the cars array else it returns false.
You can use break but what your are trying to do could be done much easier, like this:
def self.check(name)
return false if self.find_by_name(name).nil?
return true
end
This uses the database. You are trying to use Ruby at a place the database can deal with it better.
You can also use break conditional:
break if (car.name == name)
I had to do this exact same thing and I was drawing a blank. So despite this being a very old question, here's my answer:
Note: This answer assumes you don't want to return the item as it exists within the array, but instead do some processing on the item and return the result of that instead. That's how I originally read the question, I realise now that was incorrect - though this approach can be easily modified for that effect (break item insead of break output)
Since returning from blocks is dodgy (nobody likes it, and I think the rules are about to change which makes it even more fraught) this is a much nicer option:
collection.inject(nil) do |_acc, item|
output = expensive_operation(item)
break output if output
end
Note that there are lots of variants; for example, if you don't want an incidental variable, and don't mind starting a second loop in some circumstances, you can invert it like this:
collection.inject(nil) do |acc, item|
break acc if acc
expensive_operation(item)
end

How do I submit a boolean parameter in Rails?

I'm submitting a parameter show_all with the value true. This value isn't associated with a model.
My controller is assigning this parameter to an instance variable:
#show_all = params[:show_all]
However, #show_all.is_a? String, and if #show_all == true always fails.
What values does Rails parse as booleans? How can I explicitly specify that my parameter is a boolean, and not a string?
UPDATE: Rails 5:
ActiveRecord::Type::Boolean.new.deserialize('0')
UPDATE: Rails 4.2 has public API for this:
ActiveRecord::Type::Boolean.new.type_cast_from_user("0") # false
PREVIOUS ANSWER:
ActiveRecord maintains a list of representations for true/false in https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/column.rb
2.0.0-p247 :005 > ActiveRecord::ConnectionAdapters::Column.value_to_boolean("ON")
2.0.0-p247 :006 > ActiveRecord::ConnectionAdapters::Column.value_to_boolean("F")
This is not part of Rails' public API, so I wrapped it into a helper method:
class ApplicationController < ActionController::Base
private
def parse_boolean(value)
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
and added a basic test:
class ApplicationControllerTest < ActionController::TestCase
test "parses boolean params" do
refute ApplicationController.new.send(:parse_boolean, "OFF")
assert ApplicationController.new.send(:parse_boolean, "T")
end
end
I wanted to comment on zetetic answer but as I can't do that yet I'll post this as an answer.
If you use
#show_all = params[:show_all] == "1"
then you can drop ? true : false because params[:show_all] == "1" statement itself will evaluate to true or false and thus ternary operator is not needed.
This question is rather old, but since I came across this issue a couple of times, and didn't like any of the solutions proposed, I hacked something myself which allows to use multiple strings for true such as 'yes', 'on', 't' and the opposite for false.
Monkey patch the class String, and add a method to convert them to boolean, and put this file in /config/initializers as suggested here: Monkey Patching in Rails 3
class String
def to_bool
return true if ['true', '1', 'yes', 'on', 't'].include? self
return false if ['false', '0', 'no', 'off', 'f'].include? self
return nil
end
end
Notice that if the value is none of the valid ones either for true or false, then it returns nil. It's not the same to search for ?paid=false (return all records not paid) than ?paid= (I don't specify if it has to be paid or not -- so discard this).
Then, following this example, the logic in your controller would look like this:
Something.where(:paid => params[:paid].to_bool) unless params[:paid].try(:to_bool).nil?
It's pretty neat, and helps to keep controllers/models clean.
#show_all = params[:show_all] == "1" ? true : false
This should work nicely if you're passing the value in from a checkbox -- a missing key in a hash generates nil, which evaluates to false in a conditional.
EDIT
As pointed out here, the ternary operator is not necessary, so this can just be:
#show_all = params[:show_all] == "1"
You could change your equality statement to:
#show_all == "true"
If you want it to be a boolean you could create a method on the string class to convert a string to a boolean.
I think the simplest solution is to test "boolean" parameters against their String representation.
#show_all = params[:show_all]
if #show_all.to_s == "true"
# do stuff
end
Regardless of whether Rails delivers the parameter as the String "true" or "false" or an actual TrueClass or FalseClass, this test will always work.
You could just do
#show_all = params[:show_all].downcase == 'true'
It's worth noting that if you're passing down a value to an ActiveModel in Rails > 5.2, the simpler solution is to use attribute,
class Model
include ActiveModel::Attributes
attribute :show_all, :boolean
end
Model.new(show_all: '0').show_all # => false
As can be seen here.
Before 5.2 I use:
class Model
include ActiveModel::Attributes
attribute_reader :show_all
def show_all=(value)
#show_all = ActiveModel::Type::Boolean.new.cast(value)
end
end
Model.new(show_all: '0').show_all # => false
Another approach is to pass only the key without a value. Although using ActiveRecord::Type::Boolean.new.type_cast_from_user(value) is pretty neat, there might be a situation when assigning a value to the param key is redundant.
Consider the following:
On my products index view by default I want to show only scoped collection of products (e.g. those that are in the stock). That is if I want to return all the products, I may send myapp.com/products?show_all=true and typecast the show_all parameter for a boolean value.
However the opposite option - myapp.com/products?show_all=false just makes no sense since it will return the same product collection as myapp.com/products would have returned.
An alternative:
if I want to return the whole unscoped collection, then I send myapp.com/products?all and in my controller define
private
def show_all?
params.key?(:all)
end
If the key is present in params, then regardless of its value, I will know that I need to return all products, no need to typecast value.
You can add the following to your model:
def show_all= value
#show_all = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
You could convert all your boolean params to real booleans like this:
%w(show_all, show_featured).each do |bool_param|
params[bool_param.to_sym] = params[bool_param.to_sym] == "true"
end
In this solution, nil parameters would become false.
While not explicitly what the question is about I feel this is appropriately related; If you're trying to pass true boolean variables in a rails test then you're going to want the following syntax.
post :update, params: { id: user.id }, body: { attribute: true }.to_json, as: :json
I arrived at this thread looking for exactly this syntax, so I hope it helps someone looking for this as well. Credit to Lukom

Resources