rabarber 5.1.0 → 5.1.2

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +60 -41
  4. data/lib/rabarber/configuration.rb +38 -24
  5. data/lib/rabarber/controllers/concerns/authorization.rb +27 -5
  6. data/lib/rabarber/core/cache.rb +1 -1
  7. data/lib/rabarber/core/permissions.rb +1 -1
  8. data/lib/rabarber/core/roleable.rb +3 -3
  9. data/lib/rabarber/core/rule.rb +13 -9
  10. data/lib/rabarber/inputs/base.rb +35 -0
  11. data/lib/rabarber/inputs/boolean.rb +11 -0
  12. data/lib/rabarber/inputs/context.rb +33 -0
  13. data/lib/rabarber/inputs/contexts/authorizational.rb +13 -0
  14. data/lib/rabarber/inputs/dynamic_rule.rb +11 -0
  15. data/lib/rabarber/inputs/model.rb +11 -0
  16. data/lib/rabarber/inputs/non_empty_string.rb +11 -0
  17. data/lib/rabarber/inputs/role.rb +11 -0
  18. data/lib/rabarber/inputs/roles.rb +13 -0
  19. data/lib/rabarber/inputs/symbol.rb +11 -0
  20. data/lib/rabarber/models/concerns/has_roles.rb +9 -2
  21. data/lib/rabarber/models/role.rb +12 -8
  22. data/lib/rabarber/railtie.rb +1 -1
  23. data/lib/rabarber/version.rb +1 -1
  24. data/lib/rabarber.rb +23 -24
  25. data/rabarber.gemspec +2 -0
  26. metadata +39 -12
  27. data/lib/rabarber/input/action.rb +0 -21
  28. data/lib/rabarber/input/ar_model.rb +0 -23
  29. data/lib/rabarber/input/authorization_context.rb +0 -25
  30. data/lib/rabarber/input/base.rb +0 -37
  31. data/lib/rabarber/input/context.rb +0 -33
  32. data/lib/rabarber/input/dynamic_rule.rb +0 -21
  33. data/lib/rabarber/input/role.rb +0 -23
  34. data/lib/rabarber/input/roles.rb +0 -25
  35. data/lib/rabarber/input/types/boolean.rb +0 -23
  36. data/lib/rabarber/input/types/proc.rb +0 -23
  37. data/lib/rabarber/input/types/symbol.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 874cd3840269f288baec19fd5e240afdd6b06e3e5b81d703eb30e8e62283fb32
4
- data.tar.gz: 117d060ffe53ae884b657c1d79a94d9762de1de8b6726659128d7be805c77e24
3
+ metadata.gz: bcf3063fc3e0bb700ae4e372cec9807b3796863f8e94e5b2f8e79d9594d50b10
4
+ data.tar.gz: 82abf396141f483ca0ec10982bb9ef35ea8a1d79f0a1e111f2c8b483a0e480e5
5
5
  SHA512:
6
- metadata.gz: c2207e66e0743b7e5a81acfcc2707d00313fa2a8e9d8e652c7f0dc41b331d8cfa0c4d3504ff5e857606666f0145023a68e8c6d961d6285d48e36c435d4597a38
7
- data.tar.gz: ea1ab97b91cb45611f4ca24a6418484a0b7634d00b0f96cec8485ba543500fc763a33f1e0cbaf7027a8a375623e91481ca1de74d47f50cf86fe5b91bfe456d0b
6
+ metadata.gz: 023fddca28a49a89534c1d5c0b716e7f518a3a975e2b6a5883f156f75acd73a28564b475080fd5abdf7ffce294b21c1af07be276141ac747d92b9087b8cbac96
7
+ data.tar.gz: 7ae10c74e73ee844d22011a6678a8b9af579d2657d3020fe28958912c3b4a1fb033ed0a11c7af126c7367e30fcd54a1058f529b39acf0cac31391f5bc0a76146
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v5.1.2
2
+
3
+ ### Misc:
4
+
5
+ - Minor performance improvements and code cleanup
6
+ - Relaxed dependency versions
7
+
8
+ ## v5.1.1
9
+
10
+ ### Misc:
11
+
12
+ - Switched to `dry-types` for input validation
13
+ - Switched to `dry-configurable` for configuration management
14
+
1
15
  ## v5.1.0
2
16
 
3
17
  ### Features:
data/README.md CHANGED
@@ -18,8 +18,8 @@ Rabarber is a role-based authorization library for Ruby on Rails that focuses on
18
18
  **Gem Usage:**
19
19
  - [Installation](#installation)
20
20
  - [Configuration](#configuration)
21
- - [Role Management](#role-management)
22
21
  - [User Role Methods](#user-role-methods)
22
+ - [Role Management](#role-management)
23
23
  - [Controller Authorization](#controller-authorization)
24
24
  - [Dynamic Rules](#dynamic-rules)
25
25
  - [Multi-tenancy / Context](#multi-tenancy--context)
@@ -27,7 +27,7 @@ Rabarber is a role-based authorization library for Ruby on Rails that focuses on
27
27
 
28
28
 
29
29
  **Community Resources:**
30
- - [Contributing](#contributing)
30
+ - [Getting Help and Contributing](#getting-help-and-contributing)
31
31
  - [License](#license)
32
32
  - [Code of Conduct](#code-of-conduct)
33
33
  - [Old Versions](#old-versions)
@@ -80,30 +80,6 @@ To clear the role cache manually:
80
80
  Rabarber::Cache.clear
81
81
  ```
82
82
 
83
- ## Role Management
84
-
85
- ### Direct Role Operations
86
-
87
- ```rb
88
- # Create a new role
89
- Rabarber::Role.add(:admin)
90
-
91
- # Rename a role
92
- Rabarber::Role.rename(:admin, :administrator)
93
- Rabarber::Role.rename(:admin, :administrator, force: true) # Force if role is assigned to users
94
-
95
- # Remove a role
96
- Rabarber::Role.remove(:admin)
97
- Rabarber::Role.remove(:admin, force: true) # Force if role is assigned to users
98
-
99
- # List available roles
100
- Rabarber::Role.names
101
- Rabarber::Role.all_names # All roles grouped by context
102
-
103
- # Get users assigned to a role
104
- Rabarber::Role.assignees(:admin)
105
- ```
106
-
107
83
  ## User Role Methods
108
84
 
109
85
  Your user model is automatically augmented with role management methods:
@@ -137,6 +113,30 @@ user.roles
137
113
  user.all_roles
138
114
  ```
139
115
 
116
+ ## Role Management
117
+
118
+ ### Direct Role Operations
119
+
120
+ ```rb
121
+ # Create a new role
122
+ Rabarber::Role.add(:admin)
123
+
124
+ # Rename a role
125
+ Rabarber::Role.rename(:admin, :administrator)
126
+ Rabarber::Role.rename(:admin, :administrator, force: true) # Force if role is assigned to users
127
+
128
+ # Remove a role
129
+ Rabarber::Role.remove(:admin)
130
+ Rabarber::Role.remove(:admin, force: true) # Force if role is assigned to users
131
+
132
+ # List available roles
133
+ Rabarber::Role.names
134
+ Rabarber::Role.all_names # All roles grouped by context
135
+
136
+ # Get users assigned to a role
137
+ Rabarber::Role.assignees(:admin)
138
+ ```
139
+
140
140
  ## Controller Authorization
141
141
 
142
142
  ### Basic Setup
@@ -181,11 +181,6 @@ class TicketsController < ApplicationController
181
181
  def index
182
182
  # Accessible to admin, manager, and support roles
183
183
  end
184
-
185
- grant_access action: :destroy, roles: :admin
186
- def destroy
187
- # Accessible to admin role only
188
- end
189
184
  end
190
185
  ```
191
186
 
@@ -203,7 +198,9 @@ class InvoicesController < BaseController
203
198
 
204
199
  grant_access action: :index, roles: :manager
205
200
  grant_access action: :index, roles: :supervisor
206
- # Index is accessible to admin, accountant, manager, and supervisor
201
+ def index
202
+ # Index is accessible to admin, accountant, manager, and supervisor
203
+ end
207
204
  end
208
205
  ```
209
206
 
@@ -218,7 +215,14 @@ end
218
215
 
219
216
  class MixedController < ApplicationController
220
217
  grant_access action: :index # Unrestricted index action
218
+ def index
219
+ # Accessible to all users
220
+ end
221
+
221
222
  grant_access action: :show, roles: :member # Restricted show action
223
+ def show
224
+ # Accessible to members only
225
+ end
222
226
  end
223
227
  ```
224
228
 
@@ -235,13 +239,13 @@ class ApplicationController < ActionController::Base
235
239
  private
236
240
 
237
241
  def when_unauthorized
238
- # Default behavior: redirect back (HTML) or return 403 (other formats)
239
- # Custom behavior example:
240
- head :not_found # Hide existence of protected resources
242
+ head :not_found # Custom behavior to hide existence of protected resources
241
243
  end
242
244
  end
243
245
  ```
244
246
 
247
+ By default, Rabarber will redirect back (HTML format) or return 403 (other formats).
248
+
245
249
  ## Dynamic Rules
246
250
 
247
251
  Add conditional logic to authorization rules:
@@ -253,9 +257,15 @@ class OrdersController < ApplicationController
253
257
 
254
258
  # Proc-based conditions
255
259
  grant_access action: :show, roles: :client, if: -> { current_user.company_id == Order.find(params[:id]).company_id }
260
+ def show
261
+ # Accessible to company managers unless suspended, and to clients if the client's company matches the order's company
262
+ end
256
263
 
257
264
  # Dynamic-only rules (no roles required, can be used with custom policies)
258
265
  grant_access action: :index, if: -> { OrdersPolicy.new(current_user).can_access?(:index) }
266
+ def index
267
+ # Accessible to company managers unless suspended, and to users based on custom policy logic
268
+ end
259
269
 
260
270
  private
261
271
 
@@ -271,7 +281,7 @@ end
271
281
 
272
282
  ## Multi-tenancy / Context
273
283
 
274
- All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes rather than globally.
284
+ All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes. By default, context is `nil`, meaning roles are global.
275
285
 
276
286
  ### Contextual Role Assignment
277
287
 
@@ -287,6 +297,9 @@ user.assign_roles(:admin, context: Project)
287
297
  user.has_role?(:owner, context: project)
288
298
  user.has_role?(:admin, context: Project)
289
299
 
300
+ # Revoke roles within a specific context
301
+ user.revoke_roles(:owner, context: project)
302
+
290
303
  # Get roles within context
291
304
  user.roles(context: project)
292
305
  Rabarber::Role.names(context: Project)
@@ -301,9 +314,15 @@ class ProjectsController < ApplicationController
301
314
 
302
315
  # Instance-based context (method)
303
316
  grant_access action: :show, roles: :member, context: :current_project
317
+ def show
318
+ # Accessible to Project admin and members of the current project
319
+ end
304
320
 
305
321
  # Instance-based context (proc)
306
322
  grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) }
323
+ def update
324
+ # Accessible to Project admin and owner of the current project
325
+ end
307
326
 
308
327
  private
309
328
 
@@ -318,11 +337,11 @@ end
318
337
  Handle context changes when models are renamed or removed. These are irreversible data migrations.
319
338
 
320
339
  ```rb
321
- # Rename a context class (e.g., when you rename your Project model to Campaign)
322
- migrate_authorization_context!("Project", "Campaign")
340
+ # Rename a context class (e.g., when you rename your Ticket model to Task)
341
+ migrate_authorization_context!("Ticket", "Task")
323
342
 
324
- # Remove orphaned context data (e.g., when you delete a model entirely)
325
- delete_authorization_context!("DeletedModel")
343
+ # Remove orphaned context data (e.g., when you delete the Ticket model entirely)
344
+ delete_authorization_context!("Ticket")
326
345
  ```
327
346
 
328
347
  ## View Helpers
@@ -356,7 +375,7 @@ Use conditional rendering based on roles:
356
375
  <% end %>
357
376
  ```
358
377
 
359
- ## Contributing
378
+ ## Getting Help and Contributing
360
379
 
361
380
  ### Getting Help
362
381
  Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/rabarber/discussions) for:
@@ -1,35 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "singleton"
3
+ require "dry-configurable"
4
4
 
5
5
  module Rabarber
6
- class Configuration
7
- include Singleton
6
+ module Configuration
7
+ extend Dry::Configurable
8
8
 
9
- attr_reader :cache_enabled, :current_user_method
10
- attr_accessor :user_model_name
9
+ module_function
11
10
 
12
- def initialize
13
- @cache_enabled = true
14
- @current_user_method = :current_user
15
- @user_model_name = "User"
16
- end
17
-
18
- def cache_enabled=(value)
19
- @cache_enabled = Rabarber::Input::Types::Boolean.new(
20
- value, Rabarber::ConfigurationError, "Configuration `cache_enabled` must be a Boolean"
21
- ).process
22
- end
23
-
24
- def current_user_method=(method_name)
25
- @current_user_method = Rabarber::Input::Types::Symbol.new(
26
- method_name, Rabarber::ConfigurationError, "Configuration `current_user_method` must be a Symbol or a String"
27
- ).process
28
- end
11
+ setting :cache_enabled,
12
+ default: true,
13
+ reader: true,
14
+ constructor: -> (value) do
15
+ Rabarber::Inputs::Boolean.new(
16
+ value,
17
+ error: Rabarber::ConfigurationError,
18
+ message: "Invalid configuration `cache_enabled`, expected a boolean, got #{value.inspect}"
19
+ ).process
20
+ end
21
+ setting :current_user_method,
22
+ default: :current_user,
23
+ reader: true,
24
+ constructor: -> (value) do
25
+ Rabarber::Inputs::Symbol.new(
26
+ value,
27
+ error: Rabarber::ConfigurationError,
28
+ message: "Invalid configuration `current_user_method`, expected a symbol or a string, got #{value.inspect}"
29
+ ).process
30
+ end
31
+ setting :user_model_name,
32
+ default: "User",
33
+ reader: true,
34
+ constructor: -> (value) do
35
+ Rabarber::Inputs::NonEmptyString.new(
36
+ value,
37
+ error: Rabarber::ConfigurationError,
38
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{value.inspect}"
39
+ ).process
40
+ end
29
41
 
30
42
  def user_model
31
- Rabarber::Input::ArModel.new(
32
- @user_model_name, Rabarber::ConfigurationError, "Configuration `user_model_name` must be an ActiveRecord model name"
43
+ Rabarber::Inputs::Model.new(
44
+ user_model_name,
45
+ error: Rabarber::ConfigurationError,
46
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{user_model_name.inspect}"
33
47
  ).process
34
48
  end
35
49
  end
@@ -19,13 +19,35 @@ module Rabarber
19
19
  end
20
20
 
21
21
  def grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)
22
+ if_rule = binding.local_variable_get(:if)
23
+ unless_rule = binding.local_variable_get(:unless)
24
+
22
25
  Rabarber::Core::Permissions.add(
23
26
  self,
24
- Rabarber::Input::Action.new(action).process,
25
- Rabarber::Input::Roles.new(roles).process,
26
- Rabarber::Input::AuthorizationContext.new(context).process,
27
- Rabarber::Input::DynamicRule.new(binding.local_variable_get(:if)).process,
28
- Rabarber::Input::DynamicRule.new(binding.local_variable_get(:unless)).process
27
+ Rabarber::Inputs::Symbol.new(
28
+ action,
29
+ optional: true,
30
+ message: "Expected a symbol or a string, got #{action.inspect}"
31
+ ).process,
32
+ Rabarber::Inputs::Roles.new(
33
+ roles,
34
+ message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{roles.inspect}"
35
+ ).process,
36
+ Rabarber::Inputs::Contexts::Authorizational.new(
37
+ context,
38
+ error: Rabarber::InvalidContextError,
39
+ message: "Expected a Class, an instance of ActiveRecord model, a symbol, a string, or a proc, got #{context.inspect}"
40
+ ).resolve,
41
+ Rabarber::Inputs::DynamicRule.new(
42
+ if_rule,
43
+ optional: true,
44
+ message: "Expected a symbol, a string, or a proc, got #{if_rule.inspect}"
45
+ ).process,
46
+ Rabarber::Inputs::DynamicRule.new(
47
+ unless_rule,
48
+ optional: true,
49
+ message: "Expected a symbol, a string, or a proc, got #{unless_rule.inspect}"
50
+ ).process
29
51
  )
30
52
  end
31
53
  end
@@ -20,7 +20,7 @@ module Rabarber
20
20
  end
21
21
 
22
22
  def enabled?
23
- Rabarber::Configuration.instance.cache_enabled
23
+ Rabarber::Configuration.cache_enabled
24
24
  end
25
25
 
26
26
  def clear
@@ -22,7 +22,7 @@ module Rabarber
22
22
  class << self
23
23
  def add(controller, action, roles, context, dynamic_rule, negated_dynamic_rule)
24
24
  rule = Rabarber::Core::Rule.new(roles, context, dynamic_rule, negated_dynamic_rule)
25
- action ? action_rules[controller][action] += [rule] : controller_rules[controller] += [rule]
25
+ action ? action_rules[controller][action] << rule : controller_rules[controller] << rule
26
26
  end
27
27
 
28
28
  def controller_rules
@@ -4,12 +4,12 @@ module Rabarber
4
4
  module Core
5
5
  module Roleable
6
6
  def roleable
7
- current_roleable = send(Rabarber::Configuration.instance.current_user_method)
7
+ current_roleable = send(Rabarber::Configuration.current_user_method)
8
8
 
9
- unless current_roleable.is_a?(Rabarber::Configuration.instance.user_model)
9
+ unless current_roleable.is_a?(Rabarber::Configuration.user_model)
10
10
  raise(
11
11
  Rabarber::Error,
12
- "Expected `#{Rabarber::Configuration.instance.current_user_method}` to return an instance of #{Rabarber::Configuration.instance.user_model_name}, but got #{current_roleable.inspect}"
12
+ "Expected `#{Rabarber::Configuration.current_user_method}` to return an instance of #{Rabarber::Configuration.user_model_name}, got #{current_roleable.inspect}"
13
13
  )
14
14
  end
15
15
 
@@ -3,8 +3,6 @@
3
3
  module Rabarber
4
4
  module Core
5
5
  class Rule
6
- attr_reader :roles, :context, :dynamic_rule, :negated_dynamic_rule
7
-
8
6
  DEFAULT_DYNAMIC_RULE = -> { true }.freeze
9
7
  DEFAULT_NEGATED_DYNAMIC_RULE = -> { false }.freeze
10
8
  private_constant :DEFAULT_DYNAMIC_RULE, :DEFAULT_NEGATED_DYNAMIC_RULE
@@ -21,11 +19,11 @@ module Rabarber
21
19
  end
22
20
 
23
21
  def roles_permitted?(roleable, controller_instance)
24
- roles.empty? || roleable.has_role?(*roles, context: resolve_context(controller_instance))
22
+ @roles.empty? || roleable.has_role?(*@roles, context: resolve_context(controller_instance))
25
23
  end
26
24
 
27
25
  def dynamic_rules_followed?(controller_instance)
28
- execute_rule(controller_instance, dynamic_rule) && !execute_rule(controller_instance, negated_dynamic_rule)
26
+ execute_rule(controller_instance, @dynamic_rule) && !execute_rule(controller_instance, @negated_dynamic_rule)
29
27
  end
30
28
 
31
29
  private
@@ -35,11 +33,17 @@ module Rabarber
35
33
  end
36
34
 
37
35
  def resolve_context(controller_instance)
38
- case context
39
- when Proc then Rabarber::Input::Context.new(controller_instance.instance_exec(&context)).process
40
- when Symbol then Rabarber::Input::Context.new(controller_instance.send(context)).process
41
- else context
42
- end
36
+ context = case @context
37
+ when Proc then controller_instance.instance_exec(&@context)
38
+ when Symbol then controller_instance.send(@context)
39
+ else @context
40
+ end
41
+
42
+ Rabarber::Inputs::Context.new(
43
+ context,
44
+ error: Rabarber::InvalidContextError,
45
+ message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
46
+ ).resolve
43
47
  end
44
48
  end
45
49
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+
5
+ module Rabarber
6
+ module Inputs
7
+ class Base
8
+ include Dry.Types()
9
+
10
+ def initialize(value, optional: false, error: Rabarber::InvalidArgumentError, message: nil)
11
+ @value = value
12
+ @optional = optional
13
+ @error = error
14
+ @message = message
15
+ end
16
+
17
+ def process
18
+ type_checker = @optional ? type.optional : type
19
+ type_checker[@value]
20
+ rescue Dry::Types::CoercionError
21
+ raise_error
22
+ end
23
+
24
+ private
25
+
26
+ def type
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def raise_error
31
+ raise @error, @message
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Boolean < Base
6
+ private
7
+
8
+ def type = self.class::Strict::Bool
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Context < Base
6
+ def resolve
7
+ case context = process
8
+ when nil
9
+ { context_type: nil, context_id: nil }
10
+ when Class
11
+ { context_type: context.to_s, context_id: nil }
12
+ when ActiveRecord::Base
13
+ raise_error unless context.persisted?
14
+ { context_type: context.class.to_s, context_id: context.public_send(context.class.primary_key) }
15
+ else
16
+ context
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def type
23
+ self.class::Strict::Class |
24
+ self.class::Instance(ActiveRecord::Base) |
25
+ self.class::Hash.schema(
26
+ context_type: self.class::Strict::String | self.class::Nil,
27
+ context_id: self.class::Strict::String | self.class::Strict::Integer | self.class::Nil
28
+ ) |
29
+ self.class::Nil
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ module Contexts
6
+ class Authorizational < Context
7
+ private
8
+
9
+ def type = self.class::Coercible::Symbol.constrained(min_size: 1) | self.class::Instance(Proc) | super
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class DynamicRule < Base
6
+ private
7
+
8
+ def type = self.class::Coercible::Symbol.constrained(min_size: 1) | self.class::Instance(Proc)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Model < Base
6
+ private
7
+
8
+ def type = self.class::Strict::Class.constructor { _1.try(:safe_constantize) }.constrained(lt: ActiveRecord::Base)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class NonEmptyString < Base
6
+ private
7
+
8
+ def type = self.class::Strict::String.constrained(min_size: 1)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Role < Base
6
+ private
7
+
8
+ def type = self.class::Coercible::Symbol.constrained(min_size: 1, format: /\A[a-z0-9_]+\z/)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Roles < Base
6
+ private
7
+
8
+ def type
9
+ self.class::Array.of(self.class::Coercible::Symbol.constrained(min_size: 1, format: /\A[a-z0-9_]+\z/)).constructor { Kernel::Array(_1) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Inputs
5
+ class Symbol < Base
6
+ private
7
+
8
+ def type = self.class::Coercible::Symbol.constrained(min_size: 1)
9
+ end
10
+ end
11
+ end
@@ -81,11 +81,18 @@ module Rabarber
81
81
  end
82
82
 
83
83
  def process_role_names(role_names)
84
- Rabarber::Input::Roles.new(role_names).process
84
+ Rabarber::Inputs::Roles.new(
85
+ role_names,
86
+ message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{role_names.inspect}"
87
+ ).process
85
88
  end
86
89
 
87
90
  def process_context(context)
88
- Rabarber::Input::Context.new(context).process
91
+ Rabarber::Inputs::Context.new(
92
+ context,
93
+ error: Rabarber::InvalidContextError,
94
+ message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
95
+ ).resolve
89
96
  end
90
97
 
91
98
  def delete_roleable_cache(contexts:)
@@ -6,7 +6,7 @@ module Rabarber
6
6
 
7
7
  belongs_to :context, polymorphic: true, optional: true
8
8
 
9
- has_and_belongs_to_many :roleables, class_name: Rabarber::Configuration.instance.user_model_name,
9
+ has_and_belongs_to_many :roleables, class_name: Rabarber::Configuration.user_model_name,
10
10
  association_foreign_key: "roleable_id",
11
11
  join_table: "rabarber_roles_roleables"
12
12
 
@@ -63,24 +63,28 @@ module Rabarber
63
63
  end
64
64
 
65
65
  def assignees(name, context: nil)
66
- find_by(name: process_role_name(name), **process_context(context))&.roleables ||
67
- Rabarber::Configuration.instance.user_model.none
66
+ find_by(name: process_role_name(name), **process_context(context))&.roleables || Rabarber::Configuration.user_model.none
68
67
  end
69
68
 
70
69
  private
71
70
 
72
71
  def delete_roleables_cache(role, context:)
73
- role.roleables.in_batches(of: 1000) do |batch|
74
- Rabarber::Core::Cache.delete(*batch.pluck(:id).flat_map { [[_1, context], [_1, :all]] })
75
- end
72
+ Rabarber::Core::Cache.delete(*role.roleables.pluck(:id).flat_map { [[_1, context], [_1, :all]] })
76
73
  end
77
74
 
78
75
  def process_role_name(name)
79
- Rabarber::Input::Role.new(name).process
76
+ Rabarber::Inputs::Role.new(
77
+ name,
78
+ message: "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got #{name.inspect}"
79
+ ).process
80
80
  end
81
81
 
82
82
  def process_context(context)
83
- Rabarber::Input::Context.new(context).process
83
+ Rabarber::Inputs::Context.new(
84
+ context,
85
+ error: Rabarber::InvalidContextError,
86
+ message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
87
+ ).resolve
84
88
  end
85
89
  end
86
90
 
@@ -18,7 +18,7 @@ module Rabarber
18
18
  end
19
19
  end
20
20
  end
21
- user_model = Rabarber::Configuration.instance.user_model
21
+ user_model = Rabarber::Configuration.user_model
22
22
  user_model.include Rabarber::HasRoles unless user_model < Rabarber::HasRoles
23
23
  end
24
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "5.1.0"
4
+ VERSION = "5.1.2"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -1,22 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rabarber/version"
4
- require_relative "rabarber/configuration"
5
4
 
6
5
  require "active_record"
7
6
  require "active_support"
8
7
 
9
- require_relative "rabarber/input/base"
10
- require_relative "rabarber/input/action"
11
- require_relative "rabarber/input/ar_model"
12
- require_relative "rabarber/input/authorization_context"
13
- require_relative "rabarber/input/context"
14
- require_relative "rabarber/input/dynamic_rule"
15
- require_relative "rabarber/input/role"
16
- require_relative "rabarber/input/roles"
17
- require_relative "rabarber/input/types/boolean"
18
- require_relative "rabarber/input/types/proc"
19
- require_relative "rabarber/input/types/symbol"
8
+ require_relative "rabarber/inputs/base"
9
+ require_relative "rabarber/inputs/boolean"
10
+ require_relative "rabarber/inputs/context"
11
+ require_relative "rabarber/inputs/contexts/authorizational"
12
+ require_relative "rabarber/inputs/dynamic_rule"
13
+ require_relative "rabarber/inputs/model"
14
+ require_relative "rabarber/inputs/non_empty_string"
15
+ require_relative "rabarber/inputs/role"
16
+ require_relative "rabarber/inputs/roles"
17
+ require_relative "rabarber/inputs/symbol"
18
+
19
+ require_relative "rabarber/configuration"
20
+
21
+ module Rabarber
22
+ class Error < StandardError; end
23
+ class InvalidArgumentError < Rabarber::Error; end
24
+ class ConfigurationError < Rabarber::InvalidArgumentError; end
25
+ class InvalidContextError < Rabarber::InvalidArgumentError; end
26
+ class NotFoundError < Rabarber::Error; end
27
+
28
+ delegate :configure, to: Rabarber::Configuration
29
+ module_function :configure
30
+ end
20
31
 
21
32
  require_relative "rabarber/core/cache"
22
33
 
@@ -32,15 +43,3 @@ require_relative "rabarber/core/permissions"
32
43
  require_relative "rabarber/core/integrity_checker"
33
44
 
34
45
  require_relative "rabarber/railtie"
35
-
36
- module Rabarber
37
- class Error < StandardError; end
38
- class ConfigurationError < Rabarber::Error; end
39
- class InvalidArgumentError < Rabarber::Error; end
40
- class NotFoundError < Rabarber::Error; end
41
-
42
- def configure
43
- yield(Rabarber::Configuration.instance)
44
- end
45
- module_function :configure
46
- end
data/rabarber.gemspec CHANGED
@@ -21,5 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.require_paths = ["lib"]
23
23
 
24
+ spec.add_dependency "dry-configurable", "~> 1.1"
25
+ spec.add_dependency "dry-types", "~> 1.7"
24
26
  spec.add_dependency "rails", ">= 7.1", "< 8.1"
25
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rabarber
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 5.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -10,6 +10,34 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-configurable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-types
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: rails
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -50,17 +78,16 @@ files:
50
78
  - lib/rabarber/core/rule.rb
51
79
  - lib/rabarber/helpers/helpers.rb
52
80
  - lib/rabarber/helpers/migration_helpers.rb
53
- - lib/rabarber/input/action.rb
54
- - lib/rabarber/input/ar_model.rb
55
- - lib/rabarber/input/authorization_context.rb
56
- - lib/rabarber/input/base.rb
57
- - lib/rabarber/input/context.rb
58
- - lib/rabarber/input/dynamic_rule.rb
59
- - lib/rabarber/input/role.rb
60
- - lib/rabarber/input/roles.rb
61
- - lib/rabarber/input/types/boolean.rb
62
- - lib/rabarber/input/types/proc.rb
63
- - lib/rabarber/input/types/symbol.rb
81
+ - lib/rabarber/inputs/base.rb
82
+ - lib/rabarber/inputs/boolean.rb
83
+ - lib/rabarber/inputs/context.rb
84
+ - lib/rabarber/inputs/contexts/authorizational.rb
85
+ - lib/rabarber/inputs/dynamic_rule.rb
86
+ - lib/rabarber/inputs/model.rb
87
+ - lib/rabarber/inputs/non_empty_string.rb
88
+ - lib/rabarber/inputs/role.rb
89
+ - lib/rabarber/inputs/roles.rb
90
+ - lib/rabarber/inputs/symbol.rb
64
91
  - lib/rabarber/models/concerns/has_roles.rb
65
92
  - lib/rabarber/models/role.rb
66
93
  - lib/rabarber/railtie.rb
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class Action < Rabarber::Input::Base
6
- def valid?
7
- Rabarber::Input::Types::Symbol.new(value).valid? || value.nil?
8
- end
9
-
10
- private
11
-
12
- def processed_value
13
- value&.to_sym
14
- end
15
-
16
- def default_error_message
17
- "Action name must be a Symbol or a String"
18
- end
19
- end
20
- end
21
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class ArModel < Rabarber::Input::Base
6
- def valid?
7
- processed_value < ActiveRecord::Base
8
- rescue NameError
9
- false
10
- end
11
-
12
- private
13
-
14
- def processed_value
15
- value.constantize
16
- end
17
-
18
- def default_error_message
19
- "Value must be an ActiveRecord model"
20
- end
21
- end
22
- end
23
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class AuthorizationContext < Rabarber::Input::Base
6
- def valid?
7
- Rabarber::Input::Context.new(value).valid? || Rabarber::Input::DynamicRule.new(value).valid?
8
- end
9
-
10
- private
11
-
12
- def processed_value
13
- case value
14
- when String then value.to_sym
15
- when Symbol, Proc then value
16
- else Rabarber::Input::Context.new(value).process
17
- end
18
- end
19
-
20
- def default_error_message
21
- "Context must be a Class, an instance of ActiveRecord model, a Symbol, a String, or a Proc"
22
- end
23
- end
24
- end
25
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class Base
6
- attr_reader :value, :error_type, :error_message
7
-
8
- def initialize(value, error_type = Rabarber::InvalidArgumentError, error_message = nil)
9
- @value = value
10
- @error_type = error_type
11
- @error_message = error_message || default_error_message
12
- end
13
-
14
- def process
15
- valid? ? processed_value : raise_error
16
- end
17
-
18
- def valid?
19
- raise NotImplementedError
20
- end
21
-
22
- private
23
-
24
- def processed_value
25
- raise NotImplementedError
26
- end
27
-
28
- def default_error_message
29
- raise NotImplementedError
30
- end
31
-
32
- def raise_error
33
- raise error_type, error_message
34
- end
35
- end
36
- end
37
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class Context < Rabarber::Input::Base
6
- def valid?
7
- value.nil? || value.is_a?(Class) || value.is_a?(ActiveRecord::Base) && value.persisted? || already_processed?
8
- end
9
-
10
- private
11
-
12
- def processed_value
13
- case value
14
- when nil then { context_type: nil, context_id: nil }
15
- when Class then { context_type: value.to_s, context_id: nil }
16
- when ActiveRecord::Base then { context_type: value.class.to_s, context_id: value.public_send(value.class.primary_key) }
17
- else value
18
- end
19
- end
20
-
21
- def default_error_message
22
- "Context must be a Class or an instance of ActiveRecord model"
23
- end
24
-
25
- def already_processed?
26
- case value
27
- in { context_type: NilClass | String, context_id: NilClass | String | Integer } then true
28
- else false
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class DynamicRule < Rabarber::Input::Base
6
- def valid?
7
- Rabarber::Input::Types::Symbol.new(value).valid? || Rabarber::Input::Types::Proc.new(value).valid? || value.nil?
8
- end
9
-
10
- private
11
-
12
- def processed_value
13
- value.is_a?(String) ? value.to_sym : value
14
- end
15
-
16
- def default_error_message
17
- "Dynamic rule must be a Symbol, a String, or a Proc"
18
- end
19
- end
20
- end
21
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class Role < Rabarber::Input::Base
6
- REGEX = /\A[a-z0-9_]+\z/
7
-
8
- def valid?
9
- Rabarber::Input::Types::Symbol.new(value).valid? && value.to_s.match?(REGEX)
10
- end
11
-
12
- private
13
-
14
- def processed_value
15
- value.to_sym
16
- end
17
-
18
- def default_error_message
19
- "Role name must be a Symbol or a String and may only contain lowercase letters, numbers, and underscores"
20
- end
21
- end
22
- end
23
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- class Roles < Rabarber::Input::Base
6
- def value
7
- Array(super)
8
- end
9
-
10
- def valid?
11
- value.all? { |role_name| Rabarber::Input::Role.new(role_name).valid? }
12
- end
13
-
14
- private
15
-
16
- def processed_value
17
- value.map(&:to_sym)
18
- end
19
-
20
- def default_error_message
21
- "Role names must be Symbols or Strings and may only contain lowercase letters, numbers, and underscores"
22
- end
23
- end
24
- end
25
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- module Types
6
- class Boolean < Rabarber::Input::Base
7
- def valid?
8
- [true, false].include?(value)
9
- end
10
-
11
- private
12
-
13
- def processed_value
14
- value
15
- end
16
-
17
- def default_error_message
18
- "Value must be a Boolean"
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- module Types
6
- class Proc < Rabarber::Input::Base
7
- def valid?
8
- value.is_a?(::Proc)
9
- end
10
-
11
- private
12
-
13
- def processed_value
14
- value
15
- end
16
-
17
- def default_error_message
18
- "Value must be a Proc"
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Input
5
- module Types
6
- class Symbol < Rabarber::Input::Base
7
- def valid?
8
- (value.is_a?(::Symbol) || value.is_a?(String)) && value.present?
9
- end
10
-
11
- private
12
-
13
- def processed_value
14
- value.to_sym
15
- end
16
-
17
- def default_error_message
18
- "Value must be a Symbol or a String"
19
- end
20
- end
21
- end
22
- end
23
- end