I am struggling with a validation of an entity (
TimeEntry) which behavior depends on another associated entity (
Activity). To be more specific,
TimeEntry‘s validations depend on
Activity properties’ values. For example, the user has to fill the
description property of a
TimeEntry only if associated
Activity entity has
description_required property set to
You can see how I dealt with the problem in the attached code. There are a lot of custom predicates, and each one of them accesses the
ActivityReporsitory instance to read the value of certain
Activity property (so that it can be used inside a rule below). I do not like this solution: first, it’s not DRY. Second, it calls the database 5 times while it could call it only once.
The question is: Is there a better way to write validations that depend on an associated entity? How to make this code more DRY?
I know that with
dry-validations I could for example fetch
activity_id from the input params before validation, load the
Activity entity from the database, and then inject
activity object as an external dependency (like this:
scheme.with(activity: activity).call(input)). This way I wouldn’t have to access any repository inside the schema.
But First of all, I cannot find similar feature (injecting external dependencies) in Hanami::Validations. Second of all, it does not look like a good practice to “extract” certain params from the input before validation. After all, the extracted parameter would also have to be validated somewhere.
Here’s the code:
class CreateTimeEntry include Hanami::Validations::Form predicate :project_required? do |activity_id| ActivityRepository.new.find(activity_id).project_required end predicate :project_not_required? do |activity_id| ! ActivityRepository.new.find(activity_id).project_required end predicate :all_day_entry? do |activity_id| ActivityRepository.new.find(activity_id).is_all_day end predicate :hourly_entry? do |activity_id| ! ActivityRepository.new.find(activity_id).is_all_day end predicate :description_required? do |activity_id| ActivityRepository.new.find(activity_id).description_required end validations do required(:activity_id).filled(:int?) optional(:project_id).filled(:int?) optional(:hours).maybe(:decimal?) optional(:description).maybe(:str?) rule(project_presence: [:activity_id, :project_id]) do |activity_id, project_id| (activity_id.project_required? > project_id.filled?) & (activity_id.project_not_required? > project_id.none?) end rule(hours_presence: [:activity_id, :hours]) do |activity_id, hours| (activity_id.all_day_entry? > hours.none?) & (activity_id.hourly_entry? > hours.filled?) end rule(description_presence: [:activity_id, :description]) do |activity_id, description| activity_id.description_required? > description.filled? end end end class TimeEntry attr_accessor :project_id, :hours, :description end class Activity # 'Work', 'R&D', 'Paid Leave', 'Unpaid Leave' attr_accessor :name, :project_required, :description_required, :is_all_day end class TimeEntryRepository associations do belongs_to :project belongs_to :activity end end