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

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.)

Related

Rails Model Validation in abstract base class that needs to utilize name of child class

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

setting mandatory field automatically in Rails from Model callback, when and how?

I have this model which has a mandatory field which needs to be automatically set just before save. I'm struggling with the correct way to implement this:
build the logic in the controller before the save (and have validates rule in model)
build the logic in a before_save callback and have validates rule in model, but this seems to late in the flow? I do get validation errors this way.
build the logic in a before_save callback and don't define validation for this particular field
do it any of the ways above and don't assign a validates rule for the particular field
I was working on 2 since this seems like the correct way to implement this. Was considering the usage of before_validation, but I don't know what would happen when my other fields don't get validated... this could cause double assignment of the same value..
code for 2 which gives a basic idea of what I'm trying to achieve:
#category.rb
class Category < ActiveRecord::Base
before_create :set_position_number
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
I'm struggling with the correct way to implement this
The most efficient way will be to use an ActiveRecord callback hook, such as you've posted:
#app/models/category.rb
class Category < ActiveRecord::Base
before_create :your_action
private
def your_action
#fires before create
end
end
but this seems to late in the flow
As mentioned in the comments, you can see the order of the callbacks (and thus their order in the flow):
Thus, if you want to populate some data before you validate, and then validate that data, you'll be best using the before_validation callback:
#app/models/category.rb
class Category < ActiveRecord::Base
before_validation :set_position_number, on: :create
validates :position, ______________
private
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
Remember, a Rails model just populates certain attributes which are then to be either saved to the db, or validated. Rails does not care where those attributes come from; populating them before_validation is a good a source as the controller.
If you are setting a value automatically and don't take user input, you don't need validation. Write a unit test.
If the field is something like a position value, then you should indeed set it in a before_create callback.

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

How to run validations of sub-class in Single Table Inheritance?

In my application, I have a class called Budget. The budget can be of many types.. For instance, let's say that there are two budgets: FlatRateBudget and HourlyRateBudget. Both inherit from the class Budget.
This is what I get so far:
class Budget < ActiveRecord::Base
validates_presence_of :price
end
class FlatRateBudget < Budget
end
class HourlyRateBudget < Budget
validates_presence_of :quantity
end
In the console, if I do:
b = HourlyRateBudget.new(:price => 10)
b.valid?
=> false
b.errors.full_messages
=> ["Quantity can't be blank"]
As, expected.
The problem is that the "type" field, on STI, comes from params.. So i need to do something like:
b = Budget.new(:type => "HourlyRateBudget", :price => 10)
b.valid?
=> true
Which means that rails is running validations in the super-class instead of instantiating the sub class after I set up the type.
I know that is the expected behaviour, since I'm instantiating a class that dosen't need the quantity field, but I wonder if there is anyway to tell rails to run the validations for the subclass instead of the super.
You could probably solve this with a custom validator, similar to the answer on this question: Two models, one STI and a Validation However, if you can simply instantiate the intended sub-type to begin with, you would avoid the need for a custom validator altogether in this case.
As you've noticed, setting the type field alone doesn't magically change an instance from one type to another. While ActiveRecord will use the type field to instantiate the proper class upon reading the object from the database, doing it the other way around (instantiating the superclass, then changing the type field manually) doesn't have the effect of changing the object's type while your app is running - it just doesn't work that way.
The custom validation method, on the other hand, could check the type field independently, instantiate a copy of the appropriate type (based on the value of the type field), and then run .valid? on that object, resulting in the validations on the sub-class being run in a way that appears to be dynamic, even though it's actually creating an instance of the appropriate sub-class in the process.
I've done something similar.
Adapting it to your problem:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
self.class.name == 'HourlyRateBudget'
end
end
For anyone looking for example code, here's how I implemented the first answer:
validate :subclass_validations
def subclass_validations
# Typecast into subclass to check those validations
if self.class.descends_from_active_record?
subclass = self.becomes(self.type.classify.constantize)
self.errors.add(:base, "subclass validations are failing.") unless subclass.valid?
end
end
Instead of setting the type directly set the type like that... Instead, try:
new_type = params.fetch(:type)
class_type = case new_type
when "HourlyRateBudget"
HourlyRateBudget
when "FlatRateBudget"
FlatRateBudget
else
raise StandardError.new "unknown budget type: #{new_type}"
end
class_type.new(:price => 10)
You could even transform the string into its class by:
new_type.classify.constantize but if it's coming in from params, that seems a bit dangerous.
If you do this, then you'll get a class of HourlyRateBudget, otherwise it'll just be Budget.
Better yet, use type.constantize.new("10"), however this depends on that the type from params must be correct string identical to HourlyRateBudget.class.to_s
I also required the same and with the help of Bryce answer i did this:
class ActiveRecord::Base
validate :subclass_validations, :if => Proc.new{ is_sti_supported_table? }
def is_sti_supported_table?
self.class.columns_hash.include? (self.class.inheritance_column)
end
def subclass_validations
subclass = self.class.send(:compute_type, self.type)
unless subclass == self.class
subclass_obj= self.becomes(subclass)
self.errors.add(:base, subclass_obj.errors.full_messages.join(', ')) unless subclass_obj.valid?
end
end
end
Along the lines of #franzlorenzon's answer, but using duck typing to avoid referencing class type in the super class:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
false
end
end
class HourlyRateBudget < Budget
def hourly_rate?
true
end
end

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