Skip to content

Commit

Permalink
Merge pull request #137 from skryukov/inertia-share-before-action-lik…
Browse files Browse the repository at this point in the history
…e-filters

`before_action` like filters for `inertia_share`
  • Loading branch information
bknoles authored Nov 2, 2024
2 parents 498001f + 774be60 commit be142a2
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 12 deletions.
27 changes: 27 additions & 0 deletions lib/inertia_rails/action_filter.rb
Original file line number Diff line number Diff line change
@@ -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 AbstractController::ActionNotFound.new(message, controller, missing_action)
end

@actions.include?(controller.action_name)
end
end
end
65 changes: 61 additions & 4 deletions lib/inertia_rails/controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require_relative "inertia_rails"
require_relative "helper"
require_relative "action_filter"

module InertiaRails
module Controller
Expand All @@ -14,10 +15,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)
Expand Down Expand Up @@ -55,6 +65,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

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))
end

options
end

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)
end
end

def filter_to_proc(filter)
case filter
when Symbol
-> { send(filter) }
when Proc
filter
when InertiaRails::ActionFilter
-> { filter.match?(self) }
else
raise ArgumentError, "You must pass a symbol or a proc as a filter."
end
end
end

def default_render
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ class InertiaConditionalSharingController < ApplicationController
{conditionally_shared_show_prop: 1} if action_name == "show"
end

inertia_share only: :edit do
{edit_only_only_block_prop: 1}
end

inertia_share except: [:show, :index] do
{edit_only_except_block_prop: 1}
end

inertia_share if: -> { is_edit? } do
{edit_only_if_proc_prop: 1}
end

inertia_share unless: -> { !is_edit? } do
{edit_only_unless_proc_prop: 1}
end

inertia_share({edit_only_only_prop: 1}, only: :edit)

inertia_share({edit_only_if_prop: 1}, if: [:is_edit?, -> { true }])

inertia_share({edit_only_unless_prop: 1}, unless: :not_edit?)

inertia_share({edit_only_only_if_prop: 1}, only: :edit, if: -> { true })

inertia_share({edit_only_except_if_prop: 1}, except: [:index, :show], if: -> { true })

def index
render inertia: 'EmptyTestComponent', props: {
index_only_prop: 1,
Expand All @@ -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
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,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
55 changes: 55 additions & 0 deletions spec/inertia/action_filter_spec.rb
Original file line number Diff line number Diff line change
@@ -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
41 changes: 33 additions & 8 deletions spec/inertia/conditional_sharing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +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(edit_only_props.merge(normal_shared_prop: 1))
end
end

Expand Down

0 comments on commit be142a2

Please sign in to comment.