Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
特定の条件下でのカラムの値を宣言的に定義する
# app/models/concerns/forced_attribute.rb
module ForcedAttribute
extend ActiveSupport::Concern
CONDITIONAL_OPTIONS = %i[if unless on].freeze
class ForcedAttributeCallbacks
# @param attributes [Hash<symbol, any>]
def initialize(attributes = {})
@attributes = attributes.freeze
end
# 指定された値を強制的に代入する
#
# @param record [ActiveModel::Model, ActiveRecord::Base]
def before_validation(record)
record.assign_attributes(forced_attributes(record))
end
# 指定された値が強制的に代入した値か検証する
#
# @param record [ActiveModel::Model, ActiveRecord::Base]
def validate(record)
forced_attributes(record).each do |attribute, value|
validate_forced_attribute(record, attribute, value)
end
end
private
def validate_forced_attribute(record, attribute, expected)
actual = record.public_send(attribute)
return if actual == expected
record.errors.add(attribute, :not_forced_attribute, expected: expected, actual: actual)
end
def forced_attributes(record)
caster = record.class.new
@attributes.map { |attribute, value|
original_value = evaluate_force_attribute(record, value)
# 型変換をする
caster.public_send("#{attribute}=", original_value)
casted_value = caster.public_send(attribute)
[attribute, casted_value]
}.to_h
end
def evaluate_force_attribute(record, value)
if value.respond_to?(:call)
record.instance_exec(&value)
else
value
end
end
end
private_constant(:ForcedAttributeCallbacks)
class_methods do
# 指定された条件下で値を強制する
#
# @param [Hash<String, Object>] { カラム名 => 強制する値or強制する値を返すProc }
#
# @example
# forced_attribute(disabled: false)
# forced_attribute(published_at: nil, on: :create, if: :state_draft?)
#
# @return void
def forced_attribute(attributes)
options = attributes.extract!(*CONDITIONAL_OPTIONS)
callbacks = ForceAttributeCallbacks.new(attributes)
before_validation(callbacks, options)
validate(callbacks, options)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment