Skip to content

Commit be142a2

Browse files
authored
Merge pull request #137 from skryukov/inertia-share-before-action-like-filters
`before_action` like filters for `inertia_share`
2 parents 498001f + 774be60 commit be142a2

File tree

6 files changed

+217
-12
lines changed

6 files changed

+217
-12
lines changed

lib/inertia_rails/action_filter.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
#
3+
# Based on AbstractController::Callbacks::ActionFilter
4+
# https://github.com/rails/rails/blob/v7.2.0/actionpack/lib/abstract_controller/callbacks.rb#L39
5+
module InertiaRails
6+
class ActionFilter
7+
def initialize(conditional_key, actions)
8+
@conditional_key = conditional_key
9+
@actions = Array(actions).map(&:to_s).to_set
10+
end
11+
12+
def match?(controller)
13+
missing_action = @actions.find { |action| !controller.available_action?(action) }
14+
if missing_action
15+
message = <<~MSG
16+
The #{missing_action} action could not be found for the :inertia_share
17+
callback on #{controller.class.name}, but it is listed in the controller's
18+
#{@conditional_key.inspect} option.
19+
MSG
20+
21+
raise AbstractController::ActionNotFound.new(message, controller, missing_action)
22+
end
23+
24+
@actions.include?(controller.action_name)
25+
end
26+
end
27+
end

lib/inertia_rails/controller.rb

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require_relative "inertia_rails"
22
require_relative "helper"
3+
require_relative "action_filter"
34

45
module InertiaRails
56
module Controller
@@ -14,10 +15,19 @@ module Controller
1415
end
1516

1617
module ClassMethods
17-
def inertia_share(attrs = {}, &block)
18-
@inertia_share ||= []
19-
@inertia_share << attrs.freeze unless attrs.empty?
20-
@inertia_share << block if block
18+
def inertia_share(hash = nil, **props, &block)
19+
options = extract_inertia_share_options(props)
20+
return push_to_inertia_share(**(hash || props), &block) if options.empty?
21+
22+
push_to_inertia_share do
23+
next unless options[:if].all? { |filter| instance_exec(&filter) } if options[:if]
24+
next unless options[:unless].none? { |filter| instance_exec(&filter) } if options[:unless]
25+
26+
next hash unless block
27+
28+
res = instance_exec(&block)
29+
hash ? hash.merge(res) : res
30+
end
2131
end
2232

2333
def inertia_config(**attrs)
@@ -55,6 +65,53 @@ def _inertia_shared_data
5565
end.freeze
5666
end
5767
end
68+
69+
private
70+
71+
def push_to_inertia_share(**attrs, &block)
72+
@inertia_share ||= []
73+
@inertia_share << attrs.freeze unless attrs.empty?
74+
@inertia_share << block if block
75+
end
76+
77+
def extract_inertia_share_options(props)
78+
options = props.slice(:if, :unless, :only, :except)
79+
80+
return options if options.empty?
81+
82+
if props.except(:if, :unless, :only, :except).any?
83+
raise ArgumentError, "You must not mix shared data and [:if, :unless, :only, :except] options, pass data as a hash or a block."
84+
end
85+
86+
transform_inertia_share_option(options, :only, :if)
87+
transform_inertia_share_option(options, :except, :unless)
88+
89+
options.transform_values! do |filters|
90+
Array(filters).map!(&method(:filter_to_proc))
91+
end
92+
93+
options
94+
end
95+
96+
def transform_inertia_share_option(options, from, to)
97+
if (from_value = options.delete(from))
98+
filter = InertiaRails::ActionFilter.new(from, from_value)
99+
options[to] = Array(options[to]).unshift(filter)
100+
end
101+
end
102+
103+
def filter_to_proc(filter)
104+
case filter
105+
when Symbol
106+
-> { send(filter) }
107+
when Proc
108+
filter
109+
when InertiaRails::ActionFilter
110+
-> { filter.match?(self) }
111+
else
112+
raise ArgumentError, "You must pass a symbol or a proc as a filter."
113+
end
114+
end
58115
end
59116

60117
def default_render

spec/dummy/app/controllers/inertia_conditional_sharing_controller.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@ class InertiaConditionalSharingController < ApplicationController
77
{conditionally_shared_show_prop: 1} if action_name == "show"
88
end
99

10+
inertia_share only: :edit do
11+
{edit_only_only_block_prop: 1}
12+
end
13+
14+
inertia_share except: [:show, :index] do
15+
{edit_only_except_block_prop: 1}
16+
end
17+
18+
inertia_share if: -> { is_edit? } do
19+
{edit_only_if_proc_prop: 1}
20+
end
21+
22+
inertia_share unless: -> { !is_edit? } do
23+
{edit_only_unless_proc_prop: 1}
24+
end
25+
26+
inertia_share({edit_only_only_prop: 1}, only: :edit)
27+
28+
inertia_share({edit_only_if_prop: 1}, if: [:is_edit?, -> { true }])
29+
30+
inertia_share({edit_only_unless_prop: 1}, unless: :not_edit?)
31+
32+
inertia_share({edit_only_only_if_prop: 1}, only: :edit, if: -> { true })
33+
34+
inertia_share({edit_only_except_if_prop: 1}, except: [:index, :show], if: -> { true })
35+
1036
def index
1137
render inertia: 'EmptyTestComponent', props: {
1238
index_only_prop: 1,
@@ -25,9 +51,23 @@ def show_with_a_problem
2551
}
2652
end
2753

54+
def edit
55+
render inertia: 'EmptyTestComponent', props: {
56+
edit_only_prop: 1,
57+
}
58+
end
59+
2860
protected
2961

3062
def conditionally_share_a_prop
3163
self.class.inertia_share incorrectly_conditionally_shared_prop: 1
3264
end
65+
66+
def not_edit?
67+
!is_edit?
68+
end
69+
70+
def is_edit?
71+
action_name == "edit"
72+
end
3373
end

spec/dummy/config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@
5454

5555
get 'conditional_share_index' => 'inertia_conditional_sharing#index'
5656
get 'conditional_share_show' => 'inertia_conditional_sharing#show'
57+
get 'conditional_share_edit' => 'inertia_conditional_sharing#edit'
5758
get 'conditional_share_show_with_a_problem' => 'inertia_conditional_sharing#show_with_a_problem'
5859
end

spec/inertia/action_filter_spec.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# spec/lib/inertia_rails/action_filter_spec.rb
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe InertiaRails::ActionFilter do
6+
let(:controller) do
7+
instance_double(
8+
'ActionController::Base',
9+
action_name: 'current_action',
10+
class: instance_double('Class', name: 'TestController')
11+
).tap do |stub|
12+
allow(stub).to receive(:available_action?).and_return(true)
13+
allow(stub).to receive(:available_action?).with('nonexistent').and_return(false)
14+
end
15+
end
16+
17+
describe '#match?' do
18+
context 'when action exists' do
19+
it 'returns true if action matches' do
20+
filter = described_class.new(:only, 'current_action')
21+
expect(filter.match?(controller)).to be true
22+
end
23+
24+
it 'returns false if action does not match' do
25+
filter = described_class.new(:only, 'other_action')
26+
expect(filter.match?(controller)).to be false
27+
end
28+
29+
it 'handles multiple actions' do
30+
filter = described_class.new(:only, %w[current_action other actions])
31+
expect(filter.match?(controller)).to be true
32+
end
33+
34+
it 'handles symbol actions' do
35+
filter = described_class.new(:only, :current_action)
36+
expect(filter.match?(controller)).to be true
37+
end
38+
end
39+
40+
context 'when action does not exist' do
41+
it 'raises ActionNotFound with appropriate message' do
42+
filter = described_class.new(:only, :nonexistent)
43+
expected_message = <<~MSG
44+
The nonexistent action could not be found for the :inertia_share
45+
callback on TestController, but it is listed in the controller's
46+
:only option.
47+
MSG
48+
49+
expect {
50+
filter.match?(controller)
51+
}.to raise_error(AbstractController::ActionNotFound, expected_message)
52+
end
53+
end
54+
end
55+
end

spec/inertia/conditional_sharing_spec.rb

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,43 @@
22
# but it can be done by referencing the action name in an inertia_share block.
33
RSpec.describe "conditionally shared data in a controller", type: :request do
44
context "when there is data inside inertia_share only applicable to a single action" do
5-
it "does not leak the data between requests" do
6-
get conditional_share_show_path, headers: {'X-Inertia' => true}
7-
expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq({
8-
normal_shared_prop: 1,
5+
let(:edit_only_props) do
6+
{
7+
edit_only_only_block_prop: 1,
8+
edit_only_except_block_prop: 1,
9+
edit_only_if_proc_prop: 1,
10+
edit_only_unless_proc_prop: 1,
11+
edit_only_only_prop: 1,
12+
edit_only_if_prop: 1,
13+
edit_only_unless_prop: 1,
14+
edit_only_only_if_prop: 1,
15+
edit_only_except_if_prop: 1,
16+
edit_only_prop: 1,
17+
}
18+
end
19+
20+
let(:show_only_props) do
21+
{
922
show_only_prop: 1,
1023
conditionally_shared_show_prop: 1,
11-
})
24+
}
25+
end
26+
27+
let(:index_only_props) do
28+
{
29+
index_only_prop: 1,
30+
}
31+
end
32+
33+
it "does not leak the data between requests" do
34+
get conditional_share_show_path, headers: {'X-Inertia' => true}
35+
expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(show_only_props.merge(normal_shared_prop: 1))
1236

1337
get conditional_share_index_path, headers: {'X-Inertia' => true}
14-
expect(JSON.parse(response.body)['props'].deep_symbolize_keys).not_to include({
15-
conditionally_shared_show_prop: 1,
16-
})
38+
expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(index_only_props.merge(normal_shared_prop: 1))
39+
40+
get conditional_share_edit_path, headers: { 'X-Inertia' => true }
41+
expect(JSON.parse(response.body)['props'].deep_symbolize_keys).to eq(edit_only_props.merge(normal_shared_prop: 1))
1742
end
1843
end
1944

0 commit comments

Comments
 (0)