# 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