extending an apipie-rails validator - ruby-on-rails

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

Related

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 define a class like ActiveSupport::StringInquirer

Define a class which when initialized with a string e.g. 'abc' will return true if the method 'abc?' is called on it. Any other method with a trailing '?' will return false. All other methods which doesn't have a trailing '?' will raise NoMethodError
You can use method_missing to respond to messages for which there is no method.
In method_missing we can check the method name and if it ends in a ? check if it minus the ? is equal to the string (self).
When using method_missing it is custom to also define respond_to?.
class StringInquirer < String
private
def method_missing(method_name, *args, &block)
if method_name.to_s.end_with?('?')
self == method_name.to_s.delete('?')
else
super
end
end
def respond_to?(method_name, include_private = false)
method_name.to_s.ends_with('?') || super
end
end
name = StringInquirer.new('sally')
name.sally? # => true
Note this is case sensitive.
name.Sally? # => false
class NewStrInq < String
def initialize(val)
self.class.send(:define_method, "#{val}?") do
true
end
end
def method_missing(method)
method.to_s[-1] == '?' ? false : (raise NoMethodError)
end
end
class StringInquirer < String
def initialize(str)
define_singleton_method(str + '?') { true }
super(str)
end
end
name = StringInquirer.new('sally')
name.sally? # => true
name.kim? # => NoMethodError
name.nil? # => false
By raising NoMethodError for all methods ending in a question mark you will loose nil? etc.

Extending ActiveModel::Serializer with custom attributes method

I am trying to create my own attributes method called secure_attributes where I pass it an array of attributes and the minimum level the authorized user needs to be to view those attributes. I pass the current level of the authorized user as an instance_option. I'd like to extend the Serializer class so I can use this method in multiple serializers, but Im having issues.
This is what i have so far:
in config/initializers/secure_attributes.rb
module ActiveModel
class Serializer
def self.secure_attributes(attributes={}, minimum_level)
attributes.delete_if {|attr| attr == :attribute_name } unless has_access?(minimum_level)
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
end
end
and then in the individual serializer I have things like this:
secure_attributes([:id, :name, :password_hint], :guest)
and then
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
But obviously secure_attributes cannot see the has_access? method and if I put has_access inside the Serializer class, it cannot access the instance_options.
Any idea how I can accomplish what I need?
Maybe you want to do following - but I still do not get your real purpose, since you never did anything with the attributes but calling them:
module ActiveRecord
class JoshsSerializer < Serializer
class << self
def secure_attributes(attributes={}, minimum_level)
#secure_attributes = attributes
#minimum_level = minimum_level
end
attr_reader :minimum_level, :secure_attributes
end
def initialize(attr, options)
super attr, options
secure_attributes = self.class.secure_attributes.dup
secure_attributes.delete :attribute_name unless has_access?(self.class.minimum_level)
secure_attributes.each_with_object({}) do |name, hash|
if self.class._fragmented
hash[name] = self.class._fragmented.public_send(name)
else
hash[name] = send(name)
end
end
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
end
end

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?

How should i transform this concern in service object?

I have a concern allowing me to give the back end user the ability to sort elements. I use it for a few different elements. The rails community seems to be pretty vocal against concern and callbacks, i'd like to have a few pointers on how to better model the following code :
require 'active_support/concern'
module Rankable
extend ActiveSupport::Concern
included do
validates :row_order, :presence => true
scope :next_rank, lambda { |rank| where('row_order > ?',rank).order("row_order asc").limit(1)}
scope :previous_rank, lambda { |rank| where('row_order < ?',rank).order("row_order desc").limit(1)}
scope :bigger_rank, order("row_order desc").limit('1')
before_validation :assign_rank
end
def invert(target)
a = self.row_order
b = target.row_order
self.row_order = target.row_order
target.row_order = a
if self.save
if target.save
true
else
self.row_order = a
self.save
false
end
else
false
end
end
def increase_rank
return false unless self.next_rank.first && self.invert(self.next_rank.first)
end
def decrease_rank
return false unless self.previous_rank.first && self.invert(self.previous_rank.first)
end
private
def assign_default_rank
if !self.row_order
if self.class.bigger_rank.first
self.row_order = self.class.bigger_rank.first.row_order + 1
else
self.row_order=0
end
end
end
end
I think a Concern is a good choice for what you are trying to accomplish (particularly with validations and scopes because ActiveRecord does those two very well). However, if you did want to move things out of the Concern, apart from validations and scopes, here is a possibility. Just looking at the code it seems like you have a concept of rank which is represented by an integer but can become it's own object:
class Rank
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def number
#rankable.row_order
end
def increase
next_rank ? RankableInversionService.call(#rankable, next_rank) : false
end
def decrease
previous_rank ? RankableInversionService.call(#rankable, previous_rank) : false
end
private
def next_rank
#next_rank ||= #klass.next_rank.first
end
def previous_rank
#previous_rank ||= #klass.previous_rank.first
end
end
To extract out the #invert method we could create a RankableInversionService (referenced above):
class RankableInversionService
def self.call(rankable, other)
new(rankable, other).call
end
def initialize(rankable, other)
#rankable = rankable
#other = other
#original_rankable_rank = rankable.rank
#original_other_rank = other.rank
end
def call
#rankable.rank = #other.rank
#other.rank = #rankable.rank
if #rankable.save && #other.save
true
else
#rankable.rank = #original_rankable_rank
#other.rank = #original_other_rank
#rankable.save
#other.save
false
end
end
end
To extract out the callback you could have a RankableUpdateService which will assign the default rank prior to saving the object:
class RankableUpdateService
def self.call(rankable)
new(rankable).call
end
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def call
#rankable.rank = bigger_rank unless #rankable.ranked?
#rankable.save
end
private
def bigger_rank
#bigger_rank ||= #klass.bigger_rank.first.try(:rank)
end
end
Now you concern becomes:
module Rankable
extend ActiveSupport::Concern
included do
# validations
# scopes
end
def rank
#rank ||= Rank.new(self)
end
def rank=(rank)
self.row_order = rank.number; #rank = rank
end
def ranked?
rank.number.present?
end
end
I'm sure there are issues with this code if you use it as is, but you get the concept. Overall I think the only thing that might be good to do here is extracting out a Rank object, other than that it might be too much complexity that the concern encapsulates pretty nicely.

Resources