Rails Model Validation in abstract base class that needs to utilize name of child class - ruby-on-rails

I'm doing a rails setup with Single Table Inheritance, where each class has all of it's attributes stored on a JSON blob, the structure of which is defined in a json schema file unique to that class.
I want to run validations on each class against its json schema but the logic is the same for every class minus the path to the schema itself which depends on the name of the STI class and the sub class.
This setup relies on the gem 'activerecord_json_validator' which specifies a format for the validation schema files and some helpers for validation
So the desired setup is something like
/app/models/data_generating_module (the abstract STI base class which is never instantiated)
class DataGeneratingModule < ActiveRecord::Base
has_and_belongs_to_many :action_coordinates
def self.schema_path
"#{Rails.root}/app/models/json_schemas/#{superclass.to_s.underscore}/#{self.to_s.underscore}.json"
end
validates :module_data, presence: true, json: { message: ->(errors) { errors }, schema: self.schema_path }
end
app/models/sub_sti (the class that get's instantiated)
class SubSti< DataGeneratingModule
end
schema_path should evaluate to repo/app/models/json_schemas/data_generating_module/sub_sti.json"
If I leave the schema_path and validation code on the sub class it all works.
after doing this, SubSti can be instanciated but if I call .valid? on it it throws an error because when schema_path get's called it's using the values for the superclass not the subclass.
JSON::Schema::ReadFailed (Read of file at /git/repo/app/models/json_schemas/active_record/base/data_generating_module.json failed)
So basically how can I move all of this data to the abstract STI class to avoid duplicating it but where it still interpolates the proper values from the child class?

putting this inside an inherited hook and then class evaling inside of the subclass got this working:
class DataGeneratingModule < ActiveRecord::Base
def self.inherited(subclass)
subclass.class_eval do
validates :module_data, presence: true, json: { message: ->(errors) { errors }, schema: "#{Rails.root}/app/models/json_schemas/#{subclass.superclass.to_s.underscore}/#{subclass.to_s.underscore.split("/").last}.json" }
end
super
end
end

Related

Using ruby on rails validation helpers inside custom validations

Let's say I have lots of attributes that can only have a specific set of string values.
Typically we'd see the following.
class User < ApplicationRecord
validates :foo, inclusion: { in: ['some', 'array'] }
validates :bar, inclusion: { in: ['another', 'array'] }
validates :moo, inclusion: { in: ['one_more', 'array'] }
end
I have lots of these types of validations in my model and I want to DRY them up. So I tried the below but I get a error undefined method 'validates' for #User:0x00007fdc10370408.
class User < ApplicationRecord
VALIDATION_ENUMS = {
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.freeze
validate :validate_enums
def validate_enums
VALIDATION_ENUMS.each_key do |attribute|
validates attribute, inclusion: { in: VALIDATION_ENUMS[attribute] }
end
end
end
How do I get access to the ActiveModel::Validations helper methods from within my function?
Or is there a better way to do this?
Remember that validates is a class method, only executed once when the class is loaded to establish what will be validated. validate is calling an instance method.
A better way might be to execute the DRY code immediately when loading the class.
class User < ApplicationRecord
validate_enums = {
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.freeze
validate_enums.each do |key, array|
validates key, inclusion: { in: array }
end
Note that as you don't reference validate_enums ever again, you don't need to make it a class constant, which is why I didn't.
But you don't really save any lines and add complexity, so I'd stick with the explicit validates, myself.
This approach won't fly. The validation methods are class methods that modify the class itself while you are writing an instance method that get called on an instance of the class when #valid? is called.
If you want to dynamically add existing validations to the class you need to create a class method:
class User < ApplicationRecord
def self.add_inclusion_validations(hash)
# don't use each_key if you're iterating over both keys and values
hash.each do |key, values|
validates_presence_of key, in: values
end
end
add_inclusion_validations(
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
)
end
Of course you could also just skip the method completely:
class User < ApplicationRecord
{
foo: %w[foo1 foo2],
bar: %w[bar1 bar2]
}.each do |key, values|
validates_presence_of key, in: values
end
end
If what you instead want is to write a validation method that uses the existing functionality of other validations you would create a ActiveRecord::Validator or ActiveRecord::EachValidator subclass and use the existing validations there. But you really need to start by reading the guides and API docs so that you have a base understanding of how that works.

Can an inherited Trailblazer operation's contract alter validations defined by its superclass?

When a Trailblazer operation is defined by inheritance, it inherits its superclass's contract:
class Create < Trailblazer::Operation
contract do
...
end
...
end
class Update < Create
...
end
Can an inherited Trailblazer operation's contract alter validations defined by its superclass ?
This question arose because a create operation's contract defined a mandatory property that needed to be optional in the update operation:
validates :foo, presence: true
The initial thought was to somehow reverse this definition in the inherited class but there didn't appear to be a way to do this (it is possible to ignore a property in the subclass (writeable:false - book p61) but there appears to be no way to change its validity criteria).
One solution is to use an external form in each operation's contract. By extracting the form to an external class, the create operation would include and augment it like so:
contract Form do
validates :upload, presence: true
end
and the update uperation would include it simply as:
contract Form
Now the validations added in Create don't apply in Update.
You can achieve your desired result by adding an if statement to the validator:
class Create < Trailblazer::Operation
contract do
validates :upload, presence: true, if: Proc.new{ |record| !record.persisted? }
end
end
class Update < Create
end
This runs the validation only if the record has not yet been persisted to the database, so it will be skipped during the Update operation. (This assumes that you're using ActiveModel and following the normal CRUD usage pattern.)

Passing in a hash to a class without using an initialization method

I was under the, apparently incorrect, impression that when I pass a hash into a class the class requires an initialization method like this:
class Dog
attr_reader :sound
def initialize(params = {})
#sound = params[:sound]
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
But I've run into a bit of code (for creating a password digest) that works within a rails application that doesn't use an initialization method and seems to work just fine and it's kind of confuses me because when I try this anywhere else it doesn't seem to work. Here is the sample code that works (allows me to pass in a hash and initializes without an initialization method):
class User < ActiveRecord::Base
attr_reader :password
validates :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
def password=(pwd)
#password = pwd
self.password_digest = BCrypt::Password.create(pwd)
end
end
NOTE: In the create action I pass in a hash via strong params from a form that, at the end of the day, looks something like this {"email"=>"joeblow#gmail.com", "password"=>"holymolycanoli”}
In this bit of code there is no initialization method. When I try something like this (passing in a hash without an initialization method) in pry or in a repl it doesn't seem to work (for instance the following code does not work):
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
The error I get is:
wrong number of arguments (1 for 0)
Is it rails that allows me to pass in hashes like this or ActiveRecord? I'm confused as to why it works within rails within this context but generates an error outside of rails. Why does this work in rails?
If you look at the top you have this:
class Dog < ActiveRecord::Base
This causes your class Dog to inherit from ActiveRecord::Base
when it does so it gains a bunch of methods that allows you to set things up.
Now when you call for example:
Dog.create(password: 'some_password', username: 'some_username')
Your calling a method on the class object that then returns an instance of the class.
so taking your example
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
def self.create data_hash
new_dog = self.new #create new instance of dog class
new_dog.sound = data_hash[:sound] #set instance of dog classes sound
new_dog # return instance of dog class
end
end
It's essentially what we would term a factory method, a method that takes in data and returns an object based on that data.
Now I have no doubt that ActiveRecord::Base is doing something more complicated than that but that is essentially what it's doing at the most basic of levels.
I'd also like to point out that when inheriting from ActiveRecord::Base your also inheriting its 'initialize' method so you don't have to set one yourself.
The class knows what attribute methods to create based on the schema you set when you did the DB migrations for a table that matches (through rail's conventions) the class.
A lot of things happen when you subclass ActiveRecord::Base. Before looking at other issues I'm guessing that Dog is a rails ActiveRecord model and you just forgot to add
class Dog < ActiveRecord::Base

Rails: validate by calling class method of other class

I have a lot of models that contains a field called source_name. I need to implement a validator in each of them that will check if the source_name lives up to curtain conditions.
Now I also have another class called SourceNameManager. In this model I have a method called valid_source_name? which takes a source_name_name and returns true or false.
What is the simplest way to make a validation that just validates source_name by calling the external service class SourceNameManager.valid_source_name?('some_name').
I was thinking about something like:
validates :source_name, ->(record) { SourceNameManager.valid_source_name?(record.source_name) }
but I don't think that works
Create a file app/models/source_name_validator.rb:
class SourceNameValidator < ActiveModel::EachValidator
validate_each(record, attribute, value)
unless SourceNameManager.valid_source_name?(value)
record.errors[attribute] << 'is not valid'
end
end
end
Then in each model where you want to validate the source name, add:
validates :source_name, source_name: true

Subclassing ActiveRecord with permalink_fu in a rails engine

This question is related to extending class methods in Ruby, perhaps more specifically in the way that permalink_fu does so.
It appears that has_permalink on a model will not be available in a derived model. Certainly I would expect anything defined in a class to be inherited by its derived classes.
class MyScope::MyClass < ActiveRecord::Base
unloadable
self.abstract_class = true
has_permalink :name
end
class MyClass < MyScope::MyClass
unloadable
#has_permalink :name # This seems to be required
end
Is there something in the way permalink_fu mixes itself in that causes this issue?
I'm using the permalink-v.1.0.0 gem http://github.com/goncalossilva/permalink_fu
After investigating this, I can now see that the problem is related to how permalink_fu verifies it it should create a permalink or not. It verifies this by checking if the permalink_field of the class is blank or not.
What's the permalink_field? When you do
class Parent < ActiveRecord::Base
has_permalink :name
end
class Child < Parent
end
you can access the permalink by writing Parent.new.permalink or Child.new.permalink. This method name can be changed by writing
class Parent < ActiveRecord::Base
has_permalink :name 'custom_permalink_name'
end
If so, the permalink will be accessible by writing Parent.new.custom_permalink_name (or Child.new.custom_permalink_name).
What's the problem with this? The permalink_field accessor methods are defined on Parent's metaclass:
class << self
attr_accessor :permalink_field
end
When you run the has_permalink method, it calls Parent.permalink_field = 'permalink'.
The problem is that although the permalink_field method is available on all subclasses, its value is stored on the class it was called. This means that the value is not propagated to the subclasses.
So, as the permalink_field is stored on the Parent class, the Child does not inherit the value, although it inherits the accessor methods. As Child.permalink_field is blank, the should_create_permalink? returns false, and Child.create :name => 'something' does not create a permalink.
A possible solution would be to replace the attr_acessors on the metaclass with cattr_accessors on the class (lines 57 to 61 on the permalink_fu.rb file).
Replace
class << base
attr_accessor :permalink_options
attr_accessor :permalink_attributes
attr_accessor :permalink_field
end
with
base.cattr_accessor :permalink_options
base.cattr_accessor :permalink_attributes
base.cattr_accessor :permalink_field
Note that this will invalidate any possible customization on the subclass. You will no longer be able to specify different options for the subclasses, as these three attributes are shared by Parent and all its subclasses (and subsubclasses).

Resources