ArgumentError (invalid argument: nil) in Rails/GraphQL - ruby-on-rails

So I'm working through building out an app based on HowToGraphQL and stuck on the filtering of the app. I end up getting
ArgumentError (invalid argument: nil.):
app/graphql/resolvers/items_search.rb:26:in 'apply_filter'
My items_search.rb looks like:
require 'search_object/plugin/graphql'
class Resolvers::ItemsSearch
include SearchObject.module(:graphql)
scope { InventoryItem.all }
# return type
type !types[Types::InventoryItemType]
InventoryItemFilter = GraphQL::InputObjectType.define do
name 'InventoryItemFilter'
argument :OR, -> { types[InventoryItemFilter] }
argument :material_contains, types.String
# argument :balance, !types.Int
# argument :age, !types.Int
# argument :width, !types.Int
argument :roll_number_contains, types.String
argument :dye_number_contains, types.String
end
option :filter, type: InventoryItemFilter, with: :apply_filter
def apply_filter(scope, value)
branches = normalize_filters(value).reduce { |a, b| a.or(b) }
scope.merge(branches)
end
def normalize_filters(value, branches = [])
# add like SQL conditions
scope = InventoryItem.all
scope = scope.where('material LIKE ?', "%#{value['material_contains']}%") if value['material_contains']
scope = scope.where('roll_number LIKE ?', "%#{value['roll_number_contains']}%") if value['roll_number_contains']
scope = scope.where('dye_number LIKE ?', "%#{value['dye_number_contains']}%") if value['dye_number_contains']
# continue to normalize down
value['OR'].reduce(branches) { |s, v| normalize_filters(v, s) } if value['OR'].present?
branches
end
end
No idea what it's throwing up invalid argument: nil though.

Related

extending an apipie-rails validator

I'd like to extend NumberValidator to also validate for min and max values. I'd like to still call the default NumberValidator and have it return its own description (Must be a number) should it fail. Right now, I'm always getting my own implementation's description ('Must be a number between...`)
This is what I have thus far:
class NumberValidator < Apipie::Validator::BaseValidator
def initialize(param_description, argument, options)
super(param_description)
#number_validator = Apipie::Validator::NumberValidator.new(param_description)
#type = argument
#options = options
end
def validate(value)
return false unless #number_validator.valid?(value)
if (#options[:min]) && (#options[:max])
self.class.validate(value) && value.to_i.between?(#options[:min], #options[:max])
end
true
end
def self.build(param_description, argument, options, block)
if argument == :number
self.new(param_description, argument, options)
end
end
def description
"Must be a number between #{#options[:min]} and #{#options[:max]}."
end
end

Trying to filter model using public_send using name - gives me wrong number of arguments (given 1, expected 0)

Is :name a protected word on Ruby?
Model:
class Organization < ApplicationRecord
include Sortable
include Filterable
attr_filter :id, :email, :name
belongs_to :category, class_name: "OrganizationCategory"
private
def self.sortable_columns; [:created_at, :name] end
end
Filterable class:
module Filterable
extend ActiveSupport::Concern
def self.included(base)
(class << base; self; end).send(:define_method, "attr_filter") do |*attribs|
attribs.each do |attrib|
(class << base; self; end).send(:define_method, attrib) do |*args|
column = self.arel_table[attrib.to_sym]
values = args.flatten
if values.length > 1
where(column.lower.in(values.map(&:downcase)))
elsif values.length == 1
where(column.lower.eq("#{values[0].downcase}"))
end
end
end
end
end
module ClassMethods
def filtered(filtering_params)
results = where(nil)
filtering_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
end
end
I noticed that the only attribute that is giving me this error is :name, other attributes work fine. When I try to use an attribute that is not included in attr_filter list, I get the expected behaviour, which is, a No method Error rescued properly. But name is giving me ArgumentError (wrong number of arguments (given 1, expected 0)):
I get the error on this line
results = results.public_send(key, value) if value.present?
Is there any workaround to use :name alongside public_send since its such a common variable name?
If anyone stumbles across this, the problem was happening due :name already being a method defined by default to any class that inherits from active record. Thus
I came with this workaround, basically, using an constant, so that whenever I come accross a method name I cant use, this code wont break, If I have the word there.
module Filterable
extend ActiveSupport::Concern
PROTECTED_METHOD_NAMES = [:name]
def self.included(base)
(class << base; self; end).send(:define_method, "attr_filter") do |*attribs|
attribs.each do |attrib|
attrib = protected_word_method_caller(PROTECTED_METHOD_NAMES, attrib)
(class << base; self; end).send(:define_method, attrib) do |*args|
attrib.to_s.include? '_protected_word_filter'? attrib = attrib.to_s.split('_')[0] : attrib = attrib
column = self.arel_table[attrib.to_sym]
values = args.flatten
if values.length > 1
where(column.lower.in(values.map(&:downcase)))
elsif values.length == 1
where(column.lower.eq("#{values[0].downcase}"))
end
end
end
end
end
module ClassMethods
def filtered(filtering_params)
results = where(nil)
filtering_params.each do |key, value|
key = protected_word_method_caller(PROTECTED_METHOD_NAMES, key)
results = results.public_send(key, value) if value.present?
end
results
end
private
def protected_word_method_caller(protected_methods_list, method_name)
if protected_methods_list.include? method_name.to_sym
new_method_name = method_name.to_s + '_protected_word_filter'
method_name = new_method_name.to_sym
end
method_name
end
end
end

How to dynamically call scopes with 'OR' clause from an array

I have an array, and I'd like to call scopes with OR clause:
cars = ['bmw', 'audi', 'toyota']
class Car < AR
scope :from_bmw, -> { where(type: 'bmw') }
scope :from_audi, -> { where(type: 'audi') }
scope :from_toyota, -> { where(type: 'toyota') }
end
I'd like to achieve something like this:
Car.from_bmw.or(Car.from_audi).or(Car.from_toyota)
My cars array can change; in case: cars = ['toyota', 'audi'], my method should produce:
Car.from_toyota.or(Car.from_audi)
I have something like the following:
def generate(cars)
scopes = cars.map {|f| "from_#{f} "}
scopes.each do |s|
# HOW TO I ITERATE OVER HERE AND CALL EACH SCOPE???
end
end
I don't want to pass type as an argument to scope, there's a reason behind it.
def generate(cars)
return Car.none if cars.blank?
scopes = cars.map {|f| "from_#{f} "}
scope = Car.send(scopes.shift)
scopes.each do |s|
scope = scope.or(Car.send(s))
end
scope
end
Assuming the given array contains only valid type values, you could simply do that:
class Car
scope :by_type, -> (type) { where(type: type) }
end
types = ['bmw', 'audi', 'toyota']
Car.by_type(types) # => It'll generate a query using IN: SELECT * FROM cars WHERE type IN ('bmw', 'audi', 'toyota')
If you don't want to pass the array as an argument to scope for whatever reason, you could create a hash mapping the array values to valid by_type arguments.
VALID_CAR_TYPES = { volkswagen: ['vw', 'volkswagen'], bmw: ['bmw'], ... }
def sanitize_car_types(types)
types.map do |type|
VALID_CAR_TYPES.find { |k, v| v.include?(type) }.first
end.compact
end
Car.by_type(sanitize_car_types(types))

Is a ':methods' option in 'to_json' substitutable with an ':only' option?

The to_json option has options :only and :methods. The former is intended to accept attributes and the latter methods.
I have a model that has an attribute foo, which is overwritten:
class SomeModel < ActiveRecord::Base
...
def foo
# Overrides the original attribute `foo`
"the overwritten foo value"
end
end
The overwritten foo method seems to be called irrespective of which option I write the foo under.
SomeModel.first.to_json(only: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
SomeModel.first.to_json(methods: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
This seems to suggest it does not matter whether I use :only or :methods.
Is this the case? I feel something wrong with my thinking.
The source code leads to these:
File activemodel/lib/active_model/serialization.rb, line 124
def serializable_hash(options = nil)
options ||= {}
attribute_names = attributes.keys
if only = options[:only]
attribute_names &= Array(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
serializable_add_includes(options) do |association, records, opts|
hash[association.to_s] = if records.respond_to?(:to_ary)
records.to_ary.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
hash
end
File activeresource/lib/active_resource/base.rb, line 1394
def read_attribute_for_serialization(n)
attributes[n]
end
and it seems that an :only option calls attributes[n] and :methods option calls send(m). What is the difference?

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

Resources