role_fu 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b631da54e6c8f434750a6246d69bb8b7add820b2fc5f4749175572ef1c63154
4
- data.tar.gz: 570b07c252c379dad3196093527e3b30c9baba27083bc33ff4f034aa187974d8
3
+ metadata.gz: 1d30d222a87baa0b3a45fa90fb125d0be1b00c0ed6c61f84f494a55e383f9a15
4
+ data.tar.gz: a97948df483b99134fd78003033de2dfa0ab623bc4a7ea798263c7547c3ba093
5
5
  SHA512:
6
- metadata.gz: e8d5e5db755e6e38b447daa753141e01bb67b157292fceb3b363bf489ccba6662c260eabef549e535a182e8add734d683acb0a5ae05979ff2a1343d5907aead0
7
- data.tar.gz: 524e26d7e7e308cbcde01bf8e067c01a853a69a4c94a61f22c7ce31500d0eb7ed963094cdcb3cfd26ec6f88d5f9e84a1ea757ba5e706aca6e138c2655abe1b99
6
+ metadata.gz: d0add7c8ff6d0b02a2eaf3bf74903064a9ad55ea090f67914d0e69baafa4b732425809f210e2ed4627904013b22db2656f019747ed4c079938caab5caee606c2
7
+ data.tar.gz: b0f085cd793cb87574258b066c6a45b786437525a83d1df1bfb46e8bec4c75508e7620b21d78efd39ced57b744f1eb7f387181fb161d728890778ff27313d339
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
- ## [Unreleased]
1
+ ## [0.4.0] - 2026-02-08
2
+
3
+ ### Added
4
+
5
+ - **Dynamic Shortcuts**:
6
+ - Call methods like `user.is_admin?` or `user.is_manager?(org)` directly.
7
+ - Configurable pattern (`config.dynamic_shortcuts_pattern`), defaults to `is_%{role}?`.
8
+ - Custom patterns supported (e.g. `in_%{role}_group?`).
9
+ - **Enhanced Resourceable**:
10
+ - Added `has_many :users, through: :roles` association for direct user access (e.g. `org.users`).
11
+ - Added `users_grouped_by_role` to get a hash of users per role.
12
+ - Added `destroy_role(role_name)` for bulk cleanup on a resource.
13
+ - Added convenience methods/aliases: `users_with_roles`, `applied_roles`.
14
+ - **Generator**:
15
+ - Updated install template to include new configuration options.
2
16
 
3
17
  ## [0.3.3] - 2026-02-07
4
18
 
data/README.md CHANGED
@@ -235,8 +235,14 @@ class Organization < ApplicationRecord
235
235
  end
236
236
 
237
237
  org = Organization.first
238
+ org.users # All users with ANY role (via has_many :through)
238
239
  org.users_with_role(:manager)
239
- org.available_roles # ["manager", "admin"]
240
+ org.users_with_roles # All users with ANY role on this org
241
+ org.available_roles # ["manager", "admin"]
242
+ org.users_grouped_by_role # { "manager" => [user1], "admin" => [user2] }
243
+
244
+ # Cleanup
245
+ org.destroy_role(:manager) # Removes the "manager" role and all its assignments from this org
240
246
 
241
247
  # Scopes
242
248
  Organization.with_role(:manager, user)
@@ -254,9 +260,44 @@ RoleFu.configure do |config|
254
260
 
255
261
  # Enable Rolify-style permissive checks (Global roles override resource checks)
256
262
  config.global_roles_override = true
263
+
264
+ # Enable dynamic shortcuts (e.g. user.is_admin?)
265
+ # Default is nil (disabled). Recommended: "is_%{role}?"
266
+ config.dynamic_shortcuts_pattern = "is_%{role}?"
257
267
  end
258
268
  ```
259
269
 
270
+ ## Dynamic Shortcuts
271
+
272
+ RoleFu supports dynamic methods for quick role checking if you configure a pattern.
273
+
274
+ **Configuration:**
275
+
276
+ ```ruby
277
+ # In config/initializers/role_fu.rb
278
+ RoleFu.configure do |config|
279
+ config.dynamic_shortcuts_pattern = "is_%{role}?"
280
+ end
281
+ ```
282
+
283
+ **Usage:**
284
+
285
+ ```ruby
286
+ user.is_admin? # Equivalent to user.has_role?(:admin)
287
+ user.is_manager?(org) # Equivalent to user.has_role?(:manager, org)
288
+ ```
289
+
290
+ **Custom Pattern:**
291
+
292
+ If you use groups:
293
+
294
+ ```ruby
295
+ config.dynamic_shortcuts_pattern = "in_%{role}_group?"
296
+
297
+ # Usage:
298
+ user.in_admin_group? # Equivalent to user.has_role?(:admin)
299
+ ```
300
+
260
301
  ## Custom Aliases (e.g. Groups)
261
302
 
262
303
  If you prefer different terminology (e.g., "Groups" instead of "Roles"), you can alias the methods in your models:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- role_fu (0.3.3)
4
+ role_fu (0.4.0)
5
5
  activerecord (>= 7.2)
6
6
 
7
7
  GEM
@@ -104,8 +104,9 @@ GEM
104
104
  i18n (1.14.8)
105
105
  concurrent-ruby (~> 1.0)
106
106
  io-console (0.8.2)
107
- irb (1.16.0)
107
+ irb (1.17.0)
108
108
  pp (>= 0.6.0)
109
+ prism (>= 1.3.0)
109
110
  rdoc (>= 4.0.0)
110
111
  reline (>= 0.4.2)
111
112
  json (2.18.1)
@@ -205,7 +206,7 @@ GEM
205
206
  zeitwerk (~> 2.6)
206
207
  rainbow (3.1.1)
207
208
  rake (13.3.1)
208
- rdoc (7.1.0)
209
+ rdoc (7.2.0)
209
210
  erb
210
211
  psych (>= 4.0.0)
211
212
  tsort
@@ -345,7 +346,7 @@ CHECKSUMS
345
346
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
346
347
  i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
347
348
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
348
- irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
349
+ irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
349
350
  json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
350
351
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
351
352
  lefthook (2.1.0) sha256=a100dc90139806e62b9aa40cf32e9edf34f6cff10c8b55ebd14a1b0add4b86bf
@@ -386,10 +387,10 @@ CHECKSUMS
386
387
  railties (7.2.3) sha256=6eb010a6bfe6f223e783f739ddfcbdb5b88b1f3a87f7739f0a0685e466250422
387
388
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
388
389
  rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
389
- rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
390
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
390
391
  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
391
392
  reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
392
- role_fu (0.3.3)
393
+ role_fu (0.4.0)
393
394
  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
394
395
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
395
396
  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- role_fu (0.3.3)
4
+ role_fu (0.4.0)
5
5
  activerecord (>= 7.2)
6
6
 
7
7
  GEM
@@ -101,8 +101,9 @@ GEM
101
101
  i18n (1.14.8)
102
102
  concurrent-ruby (~> 1.0)
103
103
  io-console (0.8.2)
104
- irb (1.16.0)
104
+ irb (1.17.0)
105
105
  pp (>= 0.6.0)
106
+ prism (>= 1.3.0)
106
107
  rdoc (>= 4.0.0)
107
108
  reline (>= 0.4.2)
108
109
  json (2.18.1)
@@ -201,7 +202,7 @@ GEM
201
202
  zeitwerk (~> 2.6)
202
203
  rainbow (3.1.1)
203
204
  rake (13.3.1)
204
- rdoc (7.1.0)
205
+ rdoc (7.2.0)
205
206
  erb
206
207
  psych (>= 4.0.0)
207
208
  tsort
@@ -341,7 +342,7 @@ CHECKSUMS
341
342
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
342
343
  i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
343
344
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
344
- irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
345
+ irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
345
346
  json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
346
347
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
347
348
  lefthook (2.1.0) sha256=a100dc90139806e62b9aa40cf32e9edf34f6cff10c8b55ebd14a1b0add4b86bf
@@ -382,10 +383,10 @@ CHECKSUMS
382
383
  railties (8.0.4) sha256=8203d853dcffab4abcdd05c193f101676a92068075464694790f6d8f72d5cb47
383
384
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
384
385
  rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
385
- rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
386
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
386
387
  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
387
388
  reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
388
- role_fu (0.3.3)
389
+ role_fu (0.4.0)
389
390
  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
390
391
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
391
392
  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- role_fu (0.3.3)
4
+ role_fu (0.4.0)
5
5
  activerecord (>= 7.2)
6
6
 
7
7
  GEM
@@ -103,8 +103,9 @@ GEM
103
103
  i18n (1.14.8)
104
104
  concurrent-ruby (~> 1.0)
105
105
  io-console (0.8.2)
106
- irb (1.16.0)
106
+ irb (1.17.0)
107
107
  pp (>= 0.6.0)
108
+ prism (>= 1.3.0)
108
109
  rdoc (>= 4.0.0)
109
110
  reline (>= 0.4.2)
110
111
  json (2.18.1)
@@ -203,7 +204,7 @@ GEM
203
204
  zeitwerk (~> 2.6)
204
205
  rainbow (3.1.1)
205
206
  rake (13.3.1)
206
- rdoc (7.1.0)
207
+ rdoc (7.2.0)
207
208
  erb
208
209
  psych (>= 4.0.0)
209
210
  tsort
@@ -343,7 +344,7 @@ CHECKSUMS
343
344
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
344
345
  i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
345
346
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
346
- irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
347
+ irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
347
348
  json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
348
349
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
349
350
  lefthook (2.1.0) sha256=a100dc90139806e62b9aa40cf32e9edf34f6cff10c8b55ebd14a1b0add4b86bf
@@ -384,10 +385,10 @@ CHECKSUMS
384
385
  railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
385
386
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
386
387
  rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
387
- rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
388
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
388
389
  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
389
390
  reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
390
- role_fu (0.3.3)
391
+ role_fu (0.4.0)
391
392
  rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
392
393
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
393
394
  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
@@ -9,4 +9,11 @@ RoleFu.configure do |config|
9
9
 
10
10
  # The class name of your RoleAssignment model
11
11
  # config.role_assignment_class_name = "RoleAssignment"
12
+
13
+ # Enable Rolify-style permissive checks (Global roles override resource checks)
14
+ # config.global_roles_override = false
15
+
16
+ # Enable dynamic shortcuts (e.g. user.is_admin?)
17
+ # Default is "is_%{role}?". Set to nil to disable.
18
+ # config.dynamic_shortcuts_pattern = "is_%{role}?"
12
19
  end
@@ -3,12 +3,25 @@
3
3
  module RoleFu
4
4
  class Configuration
5
5
  attr_accessor :user_class_name, :role_class_name, :role_assignment_class_name, :global_roles_override
6
+ attr_reader :dynamic_shortcuts_pattern, :dynamic_shortcuts_regex
6
7
 
7
8
  def initialize
8
9
  @user_class_name = "User"
9
10
  @role_class_name = "Role"
10
11
  @role_assignment_class_name = "RoleAssignment"
11
12
  @global_roles_override = false
13
+ self.dynamic_shortcuts_pattern = "is_%{role}?"
14
+ end
15
+
16
+ def dynamic_shortcuts_pattern=(pattern)
17
+ @dynamic_shortcuts_pattern = pattern
18
+ if pattern.nil? || pattern.strip.empty?
19
+ @dynamic_shortcuts_regex = nil
20
+ else
21
+ parts = pattern.split("%{role}", -1)
22
+ regex_string = "^#{parts.map { |part| Regexp.escape(part) }.join("(?<role>.+)")}$"
23
+ @dynamic_shortcuts_regex = Regexp.new(regex_string)
24
+ end
12
25
  end
13
26
  end
14
27
 
@@ -9,6 +9,11 @@ module RoleFu
9
9
  as: :resource,
10
10
  dependent: :destroy,
11
11
  class_name: RoleFu.configuration.role_class_name
12
+
13
+ has_many :users,
14
+ through: :roles,
15
+ class_name: RoleFu.configuration.user_class_name,
16
+ source: :users
12
17
  end
13
18
 
14
19
  module ClassMethods
@@ -48,12 +53,15 @@ module RoleFu
48
53
  alias_method "users_with_any_#{singular}", :users_with_any_role
49
54
  alias_method "users_with_all_#{plural}", :users_with_all_roles
50
55
  alias_method "users_with_#{plural}", :users_with_roles
56
+ alias_method "users_grouped_by_#{singular}", :users_grouped_by_role
51
57
  alias_method "available_#{plural}", :available_roles
58
+ alias_method "applied_#{plural}", :applied_roles
52
59
  alias_method "has_#{singular}?", :has_role?
53
60
  alias_method "count_users_with_#{singular}", :count_users_with_role
54
61
  alias_method "user_has_#{singular}?", :user_has_role?
55
62
  alias_method "add_#{singular}_to_user", :add_role_to_user
56
63
  alias_method "remove_#{singular}_from_user", :remove_role_from_user
64
+ alias_method "destroy_#{singular}", :destroy_role
57
65
  end
58
66
  end
59
67
 
@@ -61,6 +69,29 @@ module RoleFu
61
69
  roles
62
70
  end
63
71
 
72
+ def destroy_role(role_name)
73
+ roles.where(name: role_name.to_s).destroy_all
74
+ end
75
+
76
+ def users_grouped_by_role
77
+ RoleFu.role_class.table_name
78
+
79
+ # Efficiently load roles and their associated users
80
+ # 1. Get all roles for this resource
81
+ # 2. Preload users for these roles
82
+
83
+ role_scope = roles.includes(:users)
84
+
85
+ results = {}
86
+ role_scope.each do |role|
87
+ results[role.name] ||= []
88
+ results[role.name].concat(role.users)
89
+ end
90
+
91
+ # Deduplicate users if a user has the same role multiple times (unlikely with distinct but safer)
92
+ results.transform_values(&:uniq)
93
+ end
94
+
64
95
  def users_with_role(role_name)
65
96
  role_table = RoleFu.role_class.table_name
66
97
  RoleFu.user_class.joins(:roles)
@@ -209,8 +209,27 @@ module RoleFu
209
209
  query.distinct
210
210
  end
211
211
 
212
+ def method_missing(method_name, *args, &block)
213
+ if (role_name = parse_dynamic_role_name(method_name))
214
+ has_role?(role_name, *args)
215
+ else
216
+ super
217
+ end
218
+ end
219
+
220
+ def respond_to_missing?(method_name, include_private = false)
221
+ parse_dynamic_role_name(method_name) || super
222
+ end
223
+
212
224
  private
213
225
 
226
+ def parse_dynamic_role_name(method_name)
227
+ return nil unless (regex = RoleFu.configuration.dynamic_shortcuts_regex)
228
+
229
+ match = method_name.to_s.match(regex)
230
+ match ? match[:role] : nil
231
+ end
232
+
214
233
  def filter_expired(relation)
215
234
  return relation unless RoleFu.role_assignment_class.column_names.include?("expires_at")
216
235
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoleFu
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: role_fu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev