Class: ExactlyOnePresentValidator
- Inherits:
-
ActiveModel::Validator
- Object
- ActiveModel::Validator
- ExactlyOnePresentValidator
- Defined in:
- app/validators/exactly_one_present_validator.rb
Overview
ExactlyOnePresentValidator
Custom validator for ensuring that exactly one of the specified fields or associations is present.
Options:
-
:fields - Array, Symbol, or Proc for database columns and custom methods.
Uses the reader method (public_send) to get the value, so any custom getter definition will be used. -
:associations - Array, Symbol, or Proc for ActiveRecord associations.
Uses ActiveRecord's association reflection to get the value directly, bypassing any custom reader methods. -
:error_key - Symbol for the error key (default: :base)
-
:message - String or Proc for custom error message
At least one of :fields or :associations must be provided. Both can be used together.
The validator supports three types of values for :fields and :associations:
-
Array: Static list of field/association names
-
Symbol: Method name that returns an array of field/association names
-
Proc/lambda: Dynamic resolution executed in the record’s context
Examples:
# Using :fields with an Array (database columns and custom methods)
class MyModel < ApplicationRecord
validates_with ExactlyOnePresentValidator, fields: %i[name url identifier]
end
# Using :associations with an Array (ActiveRecord associations)
class MyModel < ApplicationRecord
belongs_to :project, optional: true
belongs_to :group, optional: true
validates_with ExactlyOnePresentValidator, associations: %i[project group]
end
# Combining :fields and :associations
class MyModel < ApplicationRecord
belongs_to :custom_status, optional: true
validates_with ExactlyOnePresentValidator,
fields: %i[system_defined_status],
associations: %i[custom_status]
def system_defined_status
@system_defined_status ||= SystemStatus.find_by(id: system_status_id)
end
end
# Using a Symbol to call a method that returns fields dynamically
class MyModel < ApplicationRecord
validates_with ExactlyOnePresentValidator, fields: :dynamic_fields
def dynamic_fields
some_condition? ? %i[field_a field_b] : %i[field_c field_d]
end
end
# Using a Proc/lambda for dynamic field resolution
class MyModel < ApplicationRecord
validates_with ExactlyOnePresentValidator, fields: -> {
self.type == 'TypeA' ? %i[field_a field_b] : %i[field_c field_d]
}
end
# Using custom error key and message
class MyModel < ApplicationRecord
validates_with ExactlyOnePresentValidator,
fields: %i[name url],
associations: %i[project],
error_key: :custom_key,
message: 'Please provide exactly one identifier'
end
Instance Method Summary collapse
-
#initialize(*args) ⇒ ExactlyOnePresentValidator
constructor
rubocop:disable Gitlab/BoundedContexts,Gitlab/NamespacedClass – Validators can belong to multiple bounded contexts.
- #validate(record) ⇒ Object
Constructor Details
#initialize(*args) ⇒ ExactlyOnePresentValidator
rubocop:disable Gitlab/BoundedContexts,Gitlab/NamespacedClass – Validators can belong to multiple bounded contexts
75 76 77 78 79 80 81 |
# File 'app/validators/exactly_one_present_validator.rb', line 75 def initialize(*args) super return unless [:fields].blank? && [:associations].blank? raise 'ExactlyOnePresentValidator: :fields or :associations options are required' end |
Instance Method Details
#validate(record) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'app/validators/exactly_one_present_validator.rb', line 83 def validate(record) resolved_fields = resolve_option(record, :fields) resolved_associations = resolve_option(record, :associations) all_keys = resolved_fields + resolved_associations present_values = present_field_values(record, resolved_fields) + present_association_values(record, resolved_associations) return if present_values.one? add_validation_error(record, all_keys) end |