Custom ActiveModel Validator Ruby - ruby

I would like to write a custom validator for a given validates call:
class Worker
include ActiveModel::Validations
def initialize(graph_object)
#graph_object = graph_object
end
attr_accessor :graph_object
validates :graph_object, graph_object_type: {inclusion: [:ready, :active]}
end
class GraphObject
attr_accessor :state
end
I would like to validate Worker#graph_object based on a GraphObject#state. So the Worker is valid when the passed in GrapObject is in a :ready or :active state. I would like to reuse as much of the ActiveModel as possible.
Validations documentation describes the process of setting up the custom validator but I can't figure out how to do it.
I think I have to start with this:
class GraphObjectTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
end
end
options[:inclusion] = [:ready, :active]
record is the instance of the Worker(i think...)
value I have no idea (is value = record.graph_object ?)
attribute same as for value - no idea
Maybe validates :graph_object, graph_object_type: {inclusion: [:ready, :active]} isn't defined right?

OK I think I figured it out - I love puts debugging! Who needs pry!
One way of doing it would be:
class GraphObjectTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if options.key?(:inclusion) && not_included?(value.type)
record.errors.add(attribute, "wrong graph object type")
end
end
private
def not_included?(type)
!options[:inclusion].include?(type)
end
end
options[:inclusion]: [:ready, :active] array
record: instance of the Worker
value: instance of the GraphObject
attribute: :graph_object symbol

Related

What is 'valid?' in RSpec? Where can I look at it?

I've attempted to create a model, which needs to pass a series of validation tests in RSpec. However, I constantly get the error
expected #<Surveyor::Answer:0x0055db58e29260 #question=#<Double Surveyor::Question>, #value=5> to respond to `valid?`
My understanding (from here) was that 'valid?' checks that no errors were added to the model. I can't find any errors, however the message above persists.
This is my model
module Surveyor
class Answer
attr_accessor :question, :value
def initialize(params)
#question = params.fetch(:question)
#value = params.fetch(:value)
end
end
end
And the class Question
module Surveyor
class Question
attr_accessor :title, :type
def initialize(params)
#title = params.fetch(:title, nil)
#type = params.fetch(:type)
end
end
end
And this is the test I am attempting to pass
RSpec.describe Surveyor::Answer, '03: Answer validations' do
let(:question) { double(Surveyor::Question, type: 'rating') }
context "question validation" do
context "when the answer has a question" do
subject { described_class.new(question: question, value: 5) }
it { should be_valid }
end
end
Is my understanding of 'valid?' correct? Am I able to look at 'valid?' and perhaps see where I'm going wrong?
RSpec doesn't actually have a matcher called be_valid, instead it has some dynamic predicate matchers:
For any predicate method, RSpec gives you a corresponding matcher. Simply prefix the
method with be_ and remove the question mark. Examples:
expect(7).not_to be_zero # calls 7.zero?
expect([]).to be_empty # calls [].empty?
expect(x).to be_multiple_of(3) # calls x.multiple_of?(3)
so by calling it { should be_valid }, your subject has to respond to a valid? method. If you're testing an ActiveRecord model, those have a valid? method, but your model does not. So, if you want to test that your Answer is valid, you need to decide "what is a valid answer?" and write a method that checks for those conditions. If you want an API similar to Rails model, you might be interested in using ActiveModel::Validations

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Overriding methods in ActiveRecord::QueryMethods

I want to be able to override certain methods in ActiveRecord::QueryMethods for educational and experimental reasons.
Example: User is an ActiveRecord class that includes modules that overwrite the QueryMethod "order":
User.where("last_logged_in_at < ?", 1.year.ago).order("my own kind of arguments here")
However, I can't seem to get things to work. What module should I override? Something in the ARel gem, AR::Relation, or AR::QueryMethods?
I think the answer is to track down where the existing Arel order is defined.
module ActiveRecord
module QueryMethods
def order(*args)
relation = clone
relation.order_values += args.flatten unless args.blank?
relation
end
end
end
A quick test in console verifies change this will work
module ActiveRecord::QueryMethods
def order(*args)
relation = clone
if args.first
puts "ordering in ascending id"
relation.order_values += ["id ASC"]
else
puts "ordering in descending id"
relation.order_values += ["id DESC"]
end
relation
end
end
So, you can do something like this.
But my suggestion would be to create a custom my_order which keeps the original order intact, but encapsulates the same logic.
But you can define this straight on active record
class ActiveRecord::Base
class << self
def my_order(*args)
self.order(*my logic for ordering*)
end
end
end

Active Record to_json\as_json on Array of Models

First off, I am not using Rails. I am using Sinatra for this project with Active Record.
I want to be able to override either to_json or as_json on my Model class and have it define some 'default' options. For example I have the following:
class Vendor < ActiveRecord::Base
def to_json(options = {})
if options.empty?
super :only => [:id, :name]
else
super options
end
end
end
where Vendor has more attributes than just id and name. In my route I have something like the following:
#vendors = Vendor.where({})
#vendors.to_json
Here #vendors is an Array vendor objects (obviously). The returned json is, however, not invoking my to_json method and is returning all of the models attributes.
I don't really have the option of modifying the route because I am actually using a modified sinatra-rest gem (http://github.com/mikeycgto/sinatra-rest).
Any ideas on how to achieve this functionality? I could do something like the following in my sinatra-rest gem but this seems silly:
#PLURAL.collect! { |obj| obj.to_json }
Try overriding serializable_hash intead:
def serializable_hash(options = nil)
{ :id => id, :name => name }
end
More information here.
If you override as_json instead of to_json, each element in the array will format with as_json before the array is converted to JSON
I'm using the following to only expose only accessible attributes:
def as_json(options = {})
options[:only] ||= self.class.accessible_attributes.to_a
super(options)
end

Should the Applicant class "require 'mad_skills'" or "include 'mad_skills'"?

Also, what does "self.send attr" do? Is attr assumed to be a private instance variable of the ActiveEngineer class? Are there any other issues with this code in terms of Ruby logic?
class Applicant < ActiveEngineer
require 'ruby'
require 'mad_skills'
require 'oo_design'
require 'mysql'
validates :bachelors_degree
def qualified?
[:smart, :highly_productive, :curious, :driven, :team_player ].all? do
|attr|
self.send attr
end
end
end
class Employer
include TopTalent
has_millions :subscribers, :include=>:mostly_women
has_many :profits, :revenue
has_many :recent_press, :through=>[:today_show, :good_morning_america,
:new_york_times, :oprah_magazine]
belongs_to :south_park_sf
has_many :employees, :limit=>10
def apply(you)
unless you.build_successful_startups
raise "Not wanted"
end
unless you.enjoy_working_at_scale
raise "Don't bother"
end
end
def work
with small_team do
our_offerings.extend you
subscribers.send :thrill
[:scaling, :recommendation_engines, : ].each do |challenge|
assert intellectual_challenges.include? challenge
end
%w(analytics ui collaborative_filtering scraping).each{|task|
task.build }
end
end
end
def to_apply
include CoverLetter
include Resume
end
require 'mad_skills' loads the code in mad_skills.rb (or it loads mad_skills.so/.dll depending on which one exists). You need to require a file before being able to use classes, methods etc. defined in that file (though in rails files are automatically loaded when trying to access classes that have the same name as the file). Putting require inside a class definition, does not change its behaviour at all (i.e. putting it at the top of the file would not make a difference).
include MadSkills takes the module MadSkills and includes it into Applicant's inheritance chain, i.e. it makes all the methods in MadSkills available to instances of Applicant.
self.send attr executes the method with the name specified in attr on self and returns its return value. E.g. attr = "hello"; self.send(attr) will be the same as self.hello. In this case it executes the methods smart, highly_productive, curious, driven, and team_player and checks that all of them return true.

Resources