Module: TimeRangeUniqueness::ModelAdditions
- Defined in:
- lib/time_range_uniqueness/model_additions.rb
Overview
The ‘ModelAdditions` module provides a custom validation for ensuring that time ranges in ActiveRecord models are unique across records, optionally scoped by other columns.
This module is extended onto ActiveRecord::Base so that models gain a validation method to check for overlapping time ranges between records.
Example
class Event < ApplicationRecord
validates_time_range_uniqueness(
with: :event_time_range,
scope: :event_name,
message: 'cannot overlap with an existing event'
)
end
This example ensures that the ‘event_time_range` column in the `Event` model does not overlap with other records having the same `event_name`. If a new event’s time range overlaps, an error is added to the ‘event_time_range` field.
Options
-
:with- The name of the time range column (required). -
:scope- (Optional) An array of columns to scope the uniqueness check (e.g., event name). -
:message- (Optional) A custom error message when validation fails. Defaults to ‘overlaps with an existing record’ if not provided.
Methods
-
validates_time_range_uniqueness- Adds a validation for time range uniqueness. -
ModelAdditions.overlapping?- Internal helper that checks for overlapping time ranges. -
ModelAdditions.scoped_relation- Internal helper that builds the scoped relation.
Extending this onto ActiveRecord::Base adds the ability to ensure that the specified time range does not overlap with other records’ time ranges, optionally scoped by additional fields.
Class Method Summary collapse
-
.overlapping?(record, time_range_column, scope_columns) ⇒ Boolean
Checks whether the record’s time range overlaps any other record, optionally scoped.
-
.scoped_relation(record, scope_columns) ⇒ ActiveRecord::Relation
Builds the set of other records to check against, optionally scoped by the given columns.
Instance Method Summary collapse
-
#validates_time_range_uniqueness(options = {}) ⇒ Object
Adds a custom validation method to ensure that the specified time range column is unique across all records, optionally scoped by other columns.
Class Method Details
.overlapping?(record, time_range_column, scope_columns) ⇒ Boolean
Checks whether the record’s time range overlaps any other record, optionally scoped.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/time_range_uniqueness/model_additions.rb', line 69 def self.overlapping?(record, time_range_column, scope_columns) time_range = record.public_send(time_range_column) return false if time_range.nil? # A NULL scope value can never satisfy the exclusion constraint's `=` comparison, # so such a record can never conflict at the database level. Mirror that here. return false if scope_columns.any? { |col| record.public_send(col).nil? } column = record.class.connection.quote_column_name(time_range_column) bounds = time_range.exclude_end? ? '[)' : '[]' scoped_relation(record, scope_columns) .where("#{column} && tstzrange(?, ?, ?)", time_range.begin, time_range.end, bounds) .exists? end |
.scoped_relation(record, scope_columns) ⇒ ActiveRecord::Relation
Builds the set of other records to check against, optionally scoped by the given columns.
90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/time_range_uniqueness/model_additions.rb', line 90 def self.scoped_relation(record, scope_columns) klass = record.class # Pair each primary key column with its value so this works for both single # and composite primary keys (record.id is an array for composite keys). excluded = Array(klass.primary_key).zip(Array(record.id)).to_h relation = klass.where.not(excluded) scope_columns.each do |col| relation = relation.where(col => record.public_send(col)) end relation end |
Instance Method Details
#validates_time_range_uniqueness(options = {}) ⇒ Object
Adds a custom validation method to ensure that the specified time range column is unique across all records, optionally scoped by other columns.
Raises an ArgumentError if the :with option is not specified.
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/time_range_uniqueness/model_additions.rb', line 50 def validates_time_range_uniqueness( = {}) raise ArgumentError, 'You must specify the :with option with the time range column name' unless [:with] time_range_column = [:with] scope_columns = Array([:scope]) = [:message] || 'overlaps with an existing record' validate do overlapping = TimeRangeUniqueness::ModelAdditions.overlapping?(self, time_range_column, scope_columns) errors.add(time_range_column, ) if overlapping end end |