From 5cd7380d29ce0d7b35eeaa2ef51e9323695064be Mon Sep 17 00:00:00 2001 From: Anton Katunin Date: Tue, 9 Jul 2024 12:57:51 +1000 Subject: [PATCH] Nested attributes extension --- lib/active_interaction/extras.rb | 1 + lib/active_interaction/extras/all.rb | 1 + .../extras/nested_attributes.rb | 76 +++++++++++++++++++ .../extras/strong_params.rb | 51 ++++++++++--- 4 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 lib/active_interaction/extras/nested_attributes.rb diff --git a/lib/active_interaction/extras.rb b/lib/active_interaction/extras.rb index 90356bf..2093c77 100644 --- a/lib/active_interaction/extras.rb +++ b/lib/active_interaction/extras.rb @@ -23,6 +23,7 @@ module Jobs autoload(:FilterAlias, "active_interaction/extras/filter_alias") autoload(:Halt, "active_interaction/extras/halt") autoload(:ModelFields, "active_interaction/extras/model_fields") + autoload(:NestedAttributes, "active_interaction/extras/nested_attributes") autoload(:RunCallback, "active_interaction/extras/run_callback") autoload(:StrongParams, "active_interaction/extras/strong_params") autoload(:Transaction, "active_interaction/extras/transaction") diff --git a/lib/active_interaction/extras/all.rb b/lib/active_interaction/extras/all.rb index bf67dfa..ca299e4 100644 --- a/lib/active_interaction/extras/all.rb +++ b/lib/active_interaction/extras/all.rb @@ -7,6 +7,7 @@ module ActiveInteraction::Extras::All include ActiveInteraction::Extras::FilterAlias include ActiveInteraction::Extras::Halt include ActiveInteraction::Extras::ModelFields + include ActiveInteraction::Extras::NestedAttributes include ActiveInteraction::Extras::RunCallback include ActiveInteraction::Extras::StrongParams diff --git a/lib/active_interaction/extras/nested_attributes.rb b/lib/active_interaction/extras/nested_attributes.rb new file mode 100644 index 0000000..edc6fbd --- /dev/null +++ b/lib/active_interaction/extras/nested_attributes.rb @@ -0,0 +1,76 @@ +# inspired by store_model/nested_attributes.rb +module ActiveInteraction::Extras::NestedAttributes + extend ActiveSupport::Concern + + concern :InputsExtension do + def normalize(inputs) + @base.class.nested_attribute_options.each do |attribute, options| + alias_name = "#{attribute}_attributes" + next if !inputs.key?(alias_name) && !inputs.key?(alias_name.to_sym) + + value = inputs[alias_name] || inputs[alias_name.to_sym] + value = @base.class.process_nested_collection(value, options) + + inputs[attribute.to_s] = value + end + + super + end + end + + included do + ActiveInteraction::Inputs.prepend InputsExtension + + class_attribute :nested_attribute_options, default: {} + end + + class_methods do + def accepts_nested_attributes_for(*attributes) + options = attributes.extract_options! + options.reverse_merge!(allow_destroy: false, update_only: false) + + attributes.each do |attribute| + nested_attribute_options[attribute] = options + + case filters[attribute] + when ActiveInteraction::ArrayFilter + define_association_setter_for_many attribute, options + else + raise "Nested attributes are not supported for single object" + end + end + end + + def define_association_setter_for_many(association, options) + define_method "#{association}_attributes=" do |attributes| + attributes = self.class.process_nested_collection(attributes, options) + send("#{association}=", attributes) + end + end + + def process_nested_collection(attributes, options = nil) + 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) + + 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 diff --git a/lib/active_interaction/extras/strong_params.rb b/lib/active_interaction/extras/strong_params.rb index e2130e3..81ebea1 100644 --- a/lib/active_interaction/extras/strong_params.rb +++ b/lib/active_interaction/extras/strong_params.rb @@ -29,18 +29,47 @@ def initialize(inputs = {}) class_methods do def permitted_params - filters.map do |filter_name, filter| - next unless filter.options[:permit] - - case filter - when ActiveInteraction::ArrayFilter - { filter_name => [] } - when ActiveInteraction::HashFilter, ActiveInteraction::ObjectFilter - { filter_name => {} } - else - filter_name + permissions = filters.map do |filter_name, filter| + [ + permission_for_filter(filter, filter_name), + # (permission_for_filter(filter, filter.options[:as]) if filter.options[:as]) + ] + end.flatten(1).compact + + if respond_to?(:nested_attribute_options) + nested_attribute_options.each do |attribute, _| + permissions << {"#{attribute}_attributes": {}} end - end.compact + end + + permissions + end + + def permission_for_filter(filter, name = filter.name) + permit = filter.options[:permit] + return unless permit + + case filter + when ActiveInteraction::ArrayFilter + value = + if permit == true + nested_type = filter.filters.values.first + case nested_type + when ActiveInteraction::HashFilter, ActiveInteraction::ObjectFilter + {} + else + [] + end + else + permit + end + + { name => value } + when ActiveInteraction::HashFilter, ActiveInteraction::ObjectFilter + { name => {} } + else + name + end end end end