Skip to content

Instantly share code, notes, and snippets.

@henrik
Last active December 22, 2024 21:00
Show Gist options
  • Save henrik/0c01ecb112e163ffa94005d07cbff8ee to your computer and use it in GitHub Desktop.
Save henrik/0c01ecb112e163ffa94005d07cbff8ee to your computer and use it in GitHub Desktop.
Rails `with_conditions` proof-of-concept. Similar to `with_options`, but combines `if` and `unless` conditions.

UPDATE: Now a gem: https://github.com/henrik/activemodel-with_conditions


Rails with_options will overwrite if and unless conditions. So this:

with_options(if: :outer_1?, unless: :outer_2?) do
  validates :method, if: :inner_1?, unless: :inner_2?
end

Is equivalent to this:

validates :method, if: :inner_1?, unless: :inner_2?

In contrast, with_conditions (see below 👇) combines if and unless conditions. So this:

with_conditions(if: :outer_1?, unless: :outer_2?) do
  validates :method, if: :inner_1?, unless: :inner_2?
end

Is equivalent to this:

validates :method,
  if: [ :outer_1?, :inner_1? ],
  unless: [ :outer_2?, :inner_2? ]

In Active Record, arrays of validation conditions are combined:

The validation only runs when all the :if conditions and none of the :unless conditions are evaluated to true.

And the same goes for callback conditions.

class ConditionsOptionMerger < BasicObject
def initialize(context, if:, unless:)
@context = context
@if_conds = Array(::Kernel.binding.local_variable_get(:if))
@unless_conds = Array(::Kernel.binding.local_variable_get(:unless))
end
private
def method_missing(method, *, **kwargs, &)
kwargs[:if] = @if_conds + Array(kwargs[:if])
kwargs[:unless] = @unless_conds + Array(kwargs[:unless])
@context.__send__(method, *, **kwargs, &)
end
def respond_to_missing?(...) = @context.respond_to?(...)
def Array(...) = ::Kernel.Array(...)
end
class MyBaseClass
def self.with_conditions(if: nil, unless: nil, &block)
option_merger = ConditionsOptionMerger.new(self, if:, unless:)
if block
block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
else
option_merger
end
end
# Fake implementation of ActiveRecord `validate` for demo purposes.
def self.validate(name, if: nil, unless: nil)
p(validate: name, if:, unless:)
end
end
# Runnable example.
class MySubClass < MyBaseClass
with_conditions(if: :level_1?) do
validate :in_level_1
validate :in_level_1, if: :inner_cond_1?
with_conditions(if: :level_2?, unless: :not_level_2?) do
validate :in_level_2, if: :inner_cond_2?, unless: [:not_inner_a?, :not_inner_b?]
end
end
end
{:validate=>:in_level_1, :if=>[:level_1?], :unless=>[]}
{:validate=>:in_level_1, :if=>[:level_1?, :inner_cond_1?], :unless=>[]}
{:validate=>:in_level_2, :if=>[:level_1?, :level_2?, :inner_cond_2?], :unless=>[:not_level_2?, :not_inner_a?, :not_inner_b?]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment