Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

before_action like filters for inertia_share #137

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."
skryukov marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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
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