From 0875d68d0323a2c6a9eac41f4e9d80d70a1f9975 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Fri, 18 Oct 2024 10:41:19 +0300 Subject: [PATCH 1/4] Add support for :if, :unless, :only, :except options in inertia_share --- lib/inertia_rails/controller.rb | 64 +++++++++++++++++-- .../inertia_conditional_sharing_controller.rb | 40 ++++++++++++ spec/dummy/config/routes.rb | 1 + spec/inertia/conditional_sharing_spec.rb | 14 ++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/lib/inertia_rails/controller.rb b/lib/inertia_rails/controller.rb index 34bf9d0..c10aa01 100644 --- a/lib/inertia_rails/controller.rb +++ b/lib/inertia_rails/controller.rb @@ -14,10 +14,19 @@ module Controller end module ClassMethods - def inertia_share(attrs = {}, &block) - @inertia_share ||= [] - @inertia_share << attrs.freeze unless attrs.empty? - @inertia_share << block if block + def inertia_share(hash = nil, **props, &block) + options = extract_inertia_share_options(props) + return push_to_inertia_share(**(hash || props), &block) if options.empty? + + push_to_inertia_share do + next unless options[:if].all? { |filter| instance_exec(&filter) } if options[:if] + next unless options[:unless].none? { |filter| instance_exec(&filter) } if options[:unless] + + next hash unless block + + res = instance_exec(&block) + hash ? hash.merge(res) : res + end end def inertia_config(**attrs) @@ -55,6 +64,53 @@ def _inertia_shared_data end.freeze end end + + private + + def push_to_inertia_share(**attrs, &block) + @inertia_share ||= [] + @inertia_share << attrs.freeze unless attrs.empty? + @inertia_share << block if block + end + + def extract_inertia_share_options(props) + options = props.slice(:if, :unless, :only, :except) + + return options if options.empty? + + if props.except(:if, :unless, :only, :except).any? + raise ArgumentError, "You must not mix shared data and [:if, :unless, :only, :except] options, pass data as a hash or a block." + end + + extract_inertia_share_option(options, :only, :if) + extract_inertia_share_option(options, :except, :unless) + + options.transform_values! do |filters| + Array(filters).map!(&method(:filter_to_proc)) + end + + options + end + + def extract_inertia_share_option(options, from, to) + if (from_value = options.delete(from)) + filter = AbstractController::Callbacks::ActionFilter.new([:inertia_share], from, from_value) + options[to] = Array(options[to]).unshift(filter) + end + end + + def filter_to_proc(filter) + case filter + when Symbol + -> { send(filter) } + when Proc + filter + when AbstractController::Callbacks::ActionFilter + -> { filter.match?(self) } + else + raise ArgumentError, "You must pass a symbol or a proc as a filter." + end + end end def default_render diff --git a/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb b/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb index 3c3d6a3..fa24823 100644 --- a/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb +++ b/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb @@ -7,6 +7,32 @@ class InertiaConditionalSharingController < ApplicationController {conditionally_shared_show_prop: 1} if action_name == "show" end + inertia_share only: :edit do + {only_block_prop: 1} + end + + inertia_share except: [:show, :index] do + {except_block_prop: 1} + end + + inertia_share if: -> { is_edit? } do + {if_proc_prop: 1} + end + + inertia_share unless: -> { !is_edit? } do + {unless_proc_prop: 1} + end + + inertia_share({only_prop: 1}, only: :edit) + + inertia_share({if_prop: 1}, if: [:is_edit?, -> { true }]) + + inertia_share({unless_prop: 1}, unless: :not_edit?) + + inertia_share({only_if_prop: 1}, only: :edit, if: -> { true }) + + inertia_share({except_if_prop: 1}, except: [:index, :show], if: -> { true }) + def index render inertia: 'EmptyTestComponent', props: { index_only_prop: 1, @@ -25,9 +51,23 @@ def show_with_a_problem } end + def edit + render inertia: 'EmptyTestComponent', props: { + edit_only_prop: 1, + } + end + protected def conditionally_share_a_prop self.class.inertia_share incorrectly_conditionally_shared_prop: 1 end + + def not_edit? + !is_edit? + end + + def is_edit? + action_name == "edit" + end end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 0c390ec..455289f 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -52,5 +52,6 @@ get 'conditional_share_index' => 'inertia_conditional_sharing#index' get 'conditional_share_show' => 'inertia_conditional_sharing#show' + get 'conditional_share_edit' => 'inertia_conditional_sharing#edit' get 'conditional_share_show_with_a_problem' => 'inertia_conditional_sharing#show_with_a_problem' end diff --git a/spec/inertia/conditional_sharing_spec.rb b/spec/inertia/conditional_sharing_spec.rb index 05657f1..7354551 100644 --- a/spec/inertia/conditional_sharing_spec.rb +++ b/spec/inertia/conditional_sharing_spec.rb @@ -14,6 +14,20 @@ expect(JSON.parse(response.body)['props'].deep_symbolize_keys).not_to include({ conditionally_shared_show_prop: 1, }) + get conditional_share_edit_path, headers: { 'X-Inertia' => true } + expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq({ + normal_shared_prop: 1, + only_block_prop: 1, + except_block_prop: 1, + if_proc_prop: 1, + unless_proc_prop: 1, + only_prop: 1, + if_prop: 1, + unless_prop: 1, + only_if_prop: 1, + except_if_prop: 1, + edit_only_prop: 1, + }) end end From 2309b58cb26d4637adf2eda81b8fd4c00827b3ef Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Mon, 21 Oct 2024 21:49:12 +0300 Subject: [PATCH 2/4] Copy ActionFilter to support Rails < 7.1 --- lib/inertia_rails/action_filter.rb | 27 +++++++++++++++++++++++++++ lib/inertia_rails/controller.rb | 5 +++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 lib/inertia_rails/action_filter.rb diff --git a/lib/inertia_rails/action_filter.rb b/lib/inertia_rails/action_filter.rb new file mode 100644 index 0000000..c53969d --- /dev/null +++ b/lib/inertia_rails/action_filter.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +# +# Based on AbstractController::Callbacks::ActionFilter +# https://github.com/rails/rails/blob/v7.2.0/actionpack/lib/abstract_controller/callbacks.rb#L39 +module InertiaRails + class ActionFilter + def initialize(conditional_key, actions) + @conditional_key = conditional_key + @actions = Array(actions).map(&:to_s).to_set + end + + def match?(controller) + missing_action = @actions.find { |action| !controller.available_action?(action) } + if missing_action + message = <<~MSG + The #{missing_action} action could not be found for the :inertia_share + callback on #{controller.class.name}, but it is listed in the controller's + #{@conditional_key.inspect} option. + MSG + + raise ActionNotFound.new(message, controller, missing_action) + end + + @actions.include?(controller.action_name) + end + end +end diff --git a/lib/inertia_rails/controller.rb b/lib/inertia_rails/controller.rb index c10aa01..27997ab 100644 --- a/lib/inertia_rails/controller.rb +++ b/lib/inertia_rails/controller.rb @@ -1,5 +1,6 @@ require_relative "inertia_rails" require_relative "helper" +require_relative "action_filter" module InertiaRails module Controller @@ -94,7 +95,7 @@ def extract_inertia_share_options(props) def extract_inertia_share_option(options, from, to) if (from_value = options.delete(from)) - filter = AbstractController::Callbacks::ActionFilter.new([:inertia_share], from, from_value) + filter = InertiaRails::ActionFilter.new(from, from_value) options[to] = Array(options[to]).unshift(filter) end end @@ -105,7 +106,7 @@ def filter_to_proc(filter) -> { send(filter) } when Proc filter - when AbstractController::Callbacks::ActionFilter + when InertiaRails::ActionFilter -> { filter.match?(self) } else raise ArgumentError, "You must pass a symbol or a proc as a filter." From be7c56b85ef998b3a11b4bcdd2fb2d76ff076af7 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Fri, 1 Nov 2024 10:18:03 +0300 Subject: [PATCH 3/4] Update conditional sharing specs --- lib/inertia_rails/action_filter.rb | 2 +- .../inertia_conditional_sharing_controller.rb | 18 +++--- spec/inertia/action_filter_spec.rb | 55 +++++++++++++++++++ spec/inertia/conditional_sharing_spec.rb | 53 +++++++++++------- 4 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 spec/inertia/action_filter_spec.rb diff --git a/lib/inertia_rails/action_filter.rb b/lib/inertia_rails/action_filter.rb index c53969d..8ef958e 100644 --- a/lib/inertia_rails/action_filter.rb +++ b/lib/inertia_rails/action_filter.rb @@ -18,7 +18,7 @@ def match?(controller) #{@conditional_key.inspect} option. MSG - raise ActionNotFound.new(message, controller, missing_action) + raise AbstractController::ActionNotFound.new(message, controller, missing_action) end @actions.include?(controller.action_name) diff --git a/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb b/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb index fa24823..e4704b7 100644 --- a/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb +++ b/spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb @@ -8,30 +8,30 @@ class InertiaConditionalSharingController < ApplicationController end inertia_share only: :edit do - {only_block_prop: 1} + {edit_only_only_block_prop: 1} end inertia_share except: [:show, :index] do - {except_block_prop: 1} + {edit_only_except_block_prop: 1} end inertia_share if: -> { is_edit? } do - {if_proc_prop: 1} + {edit_only_if_proc_prop: 1} end inertia_share unless: -> { !is_edit? } do - {unless_proc_prop: 1} + {edit_only_unless_proc_prop: 1} end - inertia_share({only_prop: 1}, only: :edit) + inertia_share({edit_only_only_prop: 1}, only: :edit) - inertia_share({if_prop: 1}, if: [:is_edit?, -> { true }]) + inertia_share({edit_only_if_prop: 1}, if: [:is_edit?, -> { true }]) - inertia_share({unless_prop: 1}, unless: :not_edit?) + inertia_share({edit_only_unless_prop: 1}, unless: :not_edit?) - inertia_share({only_if_prop: 1}, only: :edit, if: -> { true }) + inertia_share({edit_only_only_if_prop: 1}, only: :edit, if: -> { true }) - inertia_share({except_if_prop: 1}, except: [:index, :show], if: -> { true }) + inertia_share({edit_only_except_if_prop: 1}, except: [:index, :show], if: -> { true }) def index render inertia: 'EmptyTestComponent', props: { diff --git a/spec/inertia/action_filter_spec.rb b/spec/inertia/action_filter_spec.rb new file mode 100644 index 0000000..6a35187 --- /dev/null +++ b/spec/inertia/action_filter_spec.rb @@ -0,0 +1,55 @@ +# spec/lib/inertia_rails/action_filter_spec.rb + +require 'rails_helper' + +RSpec.describe InertiaRails::ActionFilter do + let(:controller) do + instance_double( + 'ActionController::Base', + action_name: 'current_action', + class: instance_double('Class', name: 'TestController') + ).tap do |stub| + allow(stub).to receive(:available_action?).and_return(true) + allow(stub).to receive(:available_action?).with('nonexistent').and_return(false) + end + end + + describe '#match?' do + context 'when action exists' do + it 'returns true if action matches' do + filter = described_class.new(:only, 'current_action') + expect(filter.match?(controller)).to be true + end + + it 'returns false if action does not match' do + filter = described_class.new(:only, 'other_action') + expect(filter.match?(controller)).to be false + end + + it 'handles multiple actions' do + filter = described_class.new(:only, %w[current_action other actions]) + expect(filter.match?(controller)).to be true + end + + it 'handles symbol actions' do + filter = described_class.new(:only, :current_action) + expect(filter.match?(controller)).to be true + end + end + + context 'when action does not exist' do + it 'raises ActionNotFound with appropriate message' do + filter = described_class.new(:only, :nonexistent) + expected_message = <<~MSG + The nonexistent action could not be found for the :inertia_share + callback on TestController, but it is listed in the controller's + :only option. + MSG + + expect { + filter.match?(controller) + }.to raise_error(AbstractController::ActionNotFound, expected_message) + end + end + end +end diff --git a/spec/inertia/conditional_sharing_spec.rb b/spec/inertia/conditional_sharing_spec.rb index 7354551..62dc841 100644 --- a/spec/inertia/conditional_sharing_spec.rb +++ b/spec/inertia/conditional_sharing_spec.rb @@ -2,32 +2,43 @@ # but it can be done by referencing the action name in an inertia_share block. RSpec.describe "conditionally shared data in a controller", type: :request do context "when there is data inside inertia_share only applicable to a single action" do - it "does not leak the data between requests" do - get conditional_share_show_path, headers: {'X-Inertia' => true} - expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq({ - normal_shared_prop: 1, + let(:edit_only_props) do + { + edit_only_only_block_prop: 1, + edit_only_except_block_prop: 1, + edit_only_if_proc_prop: 1, + edit_only_unless_proc_prop: 1, + edit_only_only_prop: 1, + edit_only_if_prop: 1, + edit_only_unless_prop: 1, + edit_only_only_if_prop: 1, + edit_only_except_if_prop: 1, + edit_only_prop: 1, + } + end + + let(:show_only_props) do + { show_only_prop: 1, conditionally_shared_show_prop: 1, - }) + } + end + + let(:index_only_props) do + { + index_only_prop: 1, + } + end + + it "does not leak the data between requests" do + get conditional_share_show_path, headers: {'X-Inertia' => true} + expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(show_only_props.merge(normal_shared_prop: 1)) get conditional_share_index_path, headers: {'X-Inertia' => true} - expect(JSON.parse(response.body)['props'].deep_symbolize_keys).not_to include({ - conditionally_shared_show_prop: 1, - }) + expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(index_only_props.merge(normal_shared_prop: 1)) + get conditional_share_edit_path, headers: { 'X-Inertia' => true } - expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq({ - normal_shared_prop: 1, - only_block_prop: 1, - except_block_prop: 1, - if_proc_prop: 1, - unless_proc_prop: 1, - only_prop: 1, - if_prop: 1, - unless_prop: 1, - only_if_prop: 1, - except_if_prop: 1, - edit_only_prop: 1, - }) + expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(edit_only_props.merge(normal_shared_prop: 1)) end end From 774be60c41c85de8d647e862fae6df55fc6d8dc4 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Fri, 1 Nov 2024 21:18:52 +0300 Subject: [PATCH 4/4] extract_inertia_share_option -> transform_inertia_share_option --- lib/inertia_rails/controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/inertia_rails/controller.rb b/lib/inertia_rails/controller.rb index 27997ab..f63bb98 100644 --- a/lib/inertia_rails/controller.rb +++ b/lib/inertia_rails/controller.rb @@ -83,8 +83,8 @@ def extract_inertia_share_options(props) raise ArgumentError, "You must not mix shared data and [:if, :unless, :only, :except] options, pass data as a hash or a block." end - extract_inertia_share_option(options, :only, :if) - extract_inertia_share_option(options, :except, :unless) + transform_inertia_share_option(options, :only, :if) + transform_inertia_share_option(options, :except, :unless) options.transform_values! do |filters| Array(filters).map!(&method(:filter_to_proc)) @@ -93,7 +93,7 @@ def extract_inertia_share_options(props) options end - def extract_inertia_share_option(options, from, to) + def transform_inertia_share_option(options, from, to) if (from_value = options.delete(from)) filter = InertiaRails::ActionFilter.new(from, from_value) options[to] = Array(options[to]).unshift(filter)