Skip to content

Commit 7e6bad4

Browse files
committed
Use ActiveSupport::Callbacks
1 parent 8221205 commit 7e6bad4

14 files changed

+472
-117
lines changed

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Metrics/ModuleLength:
3838
- hash
3939
- heredoc
4040

41+
Naming/FileName:
42+
Exclude:
43+
- "**/lib/pundit-before.rb"
44+
4145
Style/Documentation:
4246
Enabled: false
4347

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
## [Unreleased]
22

3-
## [0.0.1] - 2023-01-03
3+
## [0.0.1] - 2023-01-15
44
- Initial release

README.md

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
![CI](https://github.com/javierav/pundit-before/workflows/CI/badge.svg)
44

5-
Add before hook to pundit
5+
Adds `before` hook to pundit policy classes to resolve things like varvet/pundit#474. Inspired by action_policy
6+
[pre-checks](https://actionpolicy.evilmartians.io/#/pre_checks).
67

78
## Installation
89

910
Add this line to your application's Gemfile:
1011

1112
```ruby
12-
gem 'pundit-before'
13+
gem "pundit-before"
1314
```
1415

1516
And then execute:
@@ -20,6 +21,173 @@ bundle install
2021

2122
## Usage
2223

24+
Use `allow!` inside callback method or block to return `true` without evaluating `edit?` method defined in policy.
25+
26+
```ruby
27+
class UserPolicy < ApplicationPolicy
28+
include Pundit::Before
29+
30+
before :check_admin
31+
32+
def edit?
33+
false
34+
end
35+
36+
private
37+
38+
def check_admin
39+
allow! if user.admin?
40+
end
41+
end
42+
43+
UserPolicy.new(User.new(admin: true), record).edit? # => true
44+
UserPolicy.new(User.new(admin: false), record).edit? # => false
45+
```
46+
47+
Use `deny!` inside callback method or block to return `false` without evaluating `edit?` method defined in policy.
48+
49+
```ruby
50+
class UserPolicy < ApplicationPolicy
51+
include Pundit::Before
52+
53+
before :check_admin
54+
55+
def edit?
56+
true
57+
end
58+
59+
private
60+
61+
def check_admin
62+
deny! unless user.admin?
63+
end
64+
end
65+
66+
UserPolicy.new(User.new(admin: true), record).edit? # => true
67+
UserPolicy.new(User.new(admin: false), record).edit? # => false
68+
```
69+
70+
Internally `before` hook is implemented as `ActiveSupport::Callbacks`, so the callback chain will halt if do any call to
71+
`allow!` or `deny!` method. It's similar as Rails controller action filters works.
72+
73+
### block form
74+
75+
```ruby
76+
class UserPolicy < ApplicationPolicy
77+
include Pundit::Before
78+
79+
before do
80+
allow! if user.admin?
81+
end
82+
83+
def edit?
84+
false
85+
end
86+
end
87+
```
88+
89+
### skip before hook
90+
91+
```ruby
92+
class UserPolicy < ApplicationPolicy
93+
include Pundit::Before
94+
95+
before :check_admin
96+
97+
def edit?
98+
false
99+
end
100+
101+
private
102+
103+
def check_admin
104+
allow! if user.admin?
105+
end
106+
end
107+
108+
class OperatorPolicy < UserPolicy
109+
skip_before :check_admin
110+
end
111+
112+
UserPolicy.new(User.new(admin: true), record).edit? # => true
113+
OperatorPolicy.new(User.new(admin: true), record).edit? # => false
114+
```
115+
116+
### using `only` modifier
117+
118+
```ruby
119+
class UserPolicy < ApplicationPolicy
120+
include Pundit::Before
121+
122+
before :check_admin, only: :update?
123+
124+
def edit?
125+
false
126+
end
127+
128+
private
129+
130+
def check_admin
131+
allow! if user.admin?
132+
end
133+
end
134+
135+
UserPolicy.new(User.new(admin: true), record).edit? # => false
136+
```
137+
138+
### using `except` modifier
139+
140+
```ruby
141+
class UserPolicy < ApplicationPolicy
142+
include Pundit::Before
143+
144+
before :check_admin, except: :edit?
145+
146+
def edit?
147+
false
148+
end
149+
150+
def destroy?
151+
false
152+
end
153+
154+
private
155+
156+
def check_admin
157+
allow! if user.admin?
158+
end
159+
end
160+
161+
UserPolicy.new(User.new(admin: true), record).edit? # => false
162+
UserPolicy.new(User.new(admin: true), record).destroy? # => true
163+
```
164+
165+
### calling multiple methods
166+
167+
```ruby
168+
class UserPolicy < BasePolicy
169+
before :check_presence, :check_admin
170+
171+
def edit?
172+
false
173+
end
174+
175+
private
176+
177+
def check_presence
178+
deny! unless user.present?
179+
end
180+
181+
def check_admin
182+
allow! if user.admin?
183+
end
184+
end
185+
186+
UserPolicy.new(nil, record).edit? # => false
187+
UserPolicy.new(User.new(admin: false), record).edit? # => false
188+
UserPolicy.new(User.new(admin: true), record).edit? # => true
189+
```
190+
23191
## Development
24192

25193
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.

lib/pundit-before.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# frozen_string_literal: true
2+
3+
require "pundit/before"

lib/pundit/before.rb

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,98 @@
11
# frozen_string_literal: true
22

3-
require "active_support/concern"
4-
require "active_support/core_ext/class/attribute"
3+
require "active_support/callbacks"
4+
require "active_support/core_ext/array/wrap"
55
require "active_support/core_ext/module/redefine_method"
66
require "active_support/core_ext/object/blank"
77

88
require_relative "before/version"
99

1010
module Pundit
1111
module Before
12-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
12+
# rubocop:disable Metrics/MethodLength
1313
def self.included(base)
1414
base.extend ClassMethods
15+
base.include ActiveSupport::Callbacks
1516

1617
base.class_eval do
17-
class_attribute :_pundit_before, default: []
18+
define_callbacks :_pundit_before, skip_after_callbacks_if_terminated: true
1819

1920
def self.method_added(method_name)
2021
super
2122

22-
return if @_pundit_before_running
2323
return unless method_name.to_s =~ /.*\?$/ && public_method_defined?(method_name)
24+
return if @_pundit_before_running
2425

2526
@_pundit_before_running = true
2627

2728
old_method = instance_method(method_name)
2829

2930
redefine_method(method_name) do
30-
result = catch :halt do
31-
(_pundit_before.presence || []).each do |name, block|
32-
name.present? ? send(name) : instance_eval(&block)
33-
end
34-
nil
35-
end
36-
37-
result.nil? ? old_method.bind(self).call : result
31+
@_pundit_before_result = nil
32+
@_pundit_before_method = method_name
33+
34+
run_callbacks :_pundit_before
35+
36+
@_pundit_before_result.nil? ? old_method.bind(self).call : @_pundit_before_result
3837
end
3938

4039
@_pundit_before_running = false
4140
end
4241
end
4342
end
44-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
43+
# rubocop:enable Metrics/MethodLength
44+
45+
class CallbackFilter
46+
def initialize(methods)
47+
@methods = Array(methods).map(&:to_sym)
48+
end
49+
50+
def match?(object)
51+
@methods.include?(object.instance_variable_get(:@_pundit_before_method).to_sym)
52+
end
53+
54+
alias after match?
55+
alias before match?
56+
alias around match?
57+
end
4558

4659
module ClassMethods
47-
def before(method_name=nil, &block)
48-
self._pundit_before = _pundit_before.dup.push([method_name, block])
60+
def before(*method_names, **options, &block)
61+
_normalize_callback_options(options)
62+
63+
if block_given?
64+
set_callback :_pundit_before, :before, **options, &block
65+
else
66+
set_callback :_pundit_before, :before, *method_names, **options
67+
end
68+
end
69+
70+
def skip_before(*method_names, **options)
71+
_normalize_callback_options(options)
72+
skip_callback :_pundit_before, :before, *method_names, **options
73+
end
74+
75+
def _normalize_callback_options(options)
76+
_normalize_callback_option(options, :only, :if)
77+
_normalize_callback_option(options, :except, :unless)
78+
end
79+
80+
def _normalize_callback_option(options, from, to)
81+
return unless (from = options.delete(from))
82+
83+
from = CallbackFilter.new(from)
84+
options[to] = Array(options[to]).unshift(from)
4985
end
5086
end
5187

5288
def allow!
53-
throw :halt, true
89+
@_pundit_before_result = true
90+
throw :abort
5491
end
5592

5693
def deny!
57-
throw :halt, false
94+
@_pundit_before_result = false
95+
throw :abort
5896
end
5997
end
6098
end

pundit-before.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
88
#
99
spec.name = "pundit-before"
1010
spec.version = Pundit::Before.version
11-
spec.summary = "Add before hook to pundit"
11+
spec.summary = "Adds before hook to pundit policy classes"
1212
spec.homepage = "https://github.com/javierav/pundit-before"
1313
spec.license = "MIT"
1414

test/test_block.rb

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,27 @@
33
require "test_helper"
44

55
class TestBlock < Minitest::Test
6-
def test_not_admin_and_not_owner
7-
user = User.new(2)
8-
record = Record.new(1, 3)
9-
10-
refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
6+
class BlockPolicy < BasePolicy
7+
before do
8+
deny! unless user.admin?
9+
end
10+
11+
def edit?
12+
true
13+
end
1114
end
1215

13-
def test_not_admin_and_owner
14-
user = User.new(3)
15-
record = Record.new(1, 3)
16-
17-
refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
18-
end
19-
20-
def test_admin_and_not_owner
16+
def test_admin
2117
user = User.new(1)
22-
record = Record.new(1, 2)
18+
policy = BlockPolicy.new(user)
2319

24-
refute_predicate BeforeWithBlockPolicy.new(user, record), :edit?
20+
assert_predicate policy, :edit?
2521
end
2622

27-
def test_admin_and_owner
28-
user = User.new(1)
29-
record = Record.new(1, 1)
23+
def test_user
24+
user = User.new(2)
25+
policy = BlockPolicy.new(user)
3026

31-
assert_predicate BeforeWithBlockPolicy.new(user, record), :edit?
27+
refute_predicate policy, :edit?
3228
end
3329
end

0 commit comments

Comments
 (0)