# frozen_string_literal: true
module StoreModel
# Contains methods for working with nested StoreModel::Model attributes.
module NestedAttributes
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods # :nodoc:
# Enables handling of nested StoreModel::Model attributes
#
# @param associations [Array] list of associations and options to define attributes, for example:
# accepts_nested_attributes_for [:suppliers, allow_destroy: true]
#
# Alternatively, use the standard Rails syntax:
#
# @param associations [Array] list of associations and attributes to define getters and setters.
#
# @param options [Hash] options not supported by StoreModel will still be passed to ActiveRecord.
#
# Supported options:
# [:allow_destroy]
# If true, destroys any members from the attributes hash with a
# _destroy key and a value that evaluates to +true+
# (e.g. 1, '1', true, or 'true'). This option is off by default.
#
# [:reject_if]
# Allows you to specify a Proc or a Symbol pointing to a method that
# checks whether a record should be built for a certain attribute hash.
# The hash is passed to the supplied Proc or the method and it should
# return either true or false. Passing :all_blank instead of a Proc
# will create a proc that will reject a record where all the attributes
# are blank excluding any value for _destroy.
#
# See https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
def accepts_nested_attributes_for(*attributes)
global_options = attributes.extract_options!
attributes.each do |attribute, options|
case attribute_types[attribute.to_s]
when Types::OneBase, Types::ManyBase
define_store_model_attr_accessors(attribute, options || global_options)
else
super(attribute, options || global_options)
end
end
end
private
def define_store_model_attr_accessors(attribute, options)
case attribute_types[attribute.to_s]
when Types::OneBase
define_association_setter_for_single(attribute, options)
alias_method "#{attribute}_attributes=", "#{attribute}="
when Types::ManyBase
define_association_setter_for_many(attribute, options)
end
define_attr_accessor_for_destroy(attribute, options)
end
def define_attr_accessor_for_destroy(association, options)
return unless options&.dig(:allow_destroy)
attribute_types[association.to_s].model_klass.class_eval do
attr_accessor :_destroy
end
end
def define_association_setter_for_many(association, options)
define_method "#{association}_attributes=" do |attributes|
assign_nested_attributes_for_collection_association(association, attributes, options)
end
end
def define_association_setter_for_single(association, options)
return unless options&.dig(:allow_destroy)
define_method "#{association}=" do |attributes|
if ActiveRecord::Type::Boolean.new.cast(attributes.stringify_keys.dig("_destroy"))
super(nil)
else
super(attributes)
end
end
end
end
def assign_nested_attributes_for_collection_association(association, attributes, options)
attributes = attributes.values if attributes.is_a?(Hash)
if options&.dig(:allow_destroy)
attributes.reject! do |attribute|
ActiveRecord::Type::Boolean.new.cast(attribute.stringify_keys.dig("_destroy"))
end
end
attributes.reject! { |attribute| call_reject_if(attribute, options[:reject_if]) } if options&.dig(:reject_if)
send("#{association}=", attributes)
end
def call_reject_if(attributes, callback)
callback = ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC if callback == :all_blank
case callback
when Symbol
method(callback).arity.zero? ? send(callback) : send(callback, attributes)
when Proc
callback.call(attributes)
end
end
end
end