rabarber 6.0.2 → 6.1.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.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Rabarber
6
+ class Engine < Rails::Engine
7
+ initializer "rabarber.include_roleable" do
8
+ Rails.autoloaders.each do |autoloader|
9
+ autoloader.on_load do |cpath, klass, _abspath|
10
+ klass.include Rabarber::Roleable if cpath == Rabarber::Configuration.user_model_name
11
+ end
12
+ end
13
+ end
14
+
15
+ initializer "rabarber.to_prepare" do |app|
16
+ app.config.to_prepare do
17
+ Rabarber::Core::Permissions.reset! unless app.config.eager_load
18
+ end
19
+ end
20
+
21
+ initializer "rabarber.deprecator" do |app|
22
+ app.deprecators[:rabarber] = Rabarber.deprecator
23
+ end
24
+
25
+ initializer "rabarber.extend_migration_helpers" do
26
+ ActiveSupport.on_load :active_record do
27
+ ActiveRecord::Migration.include Rabarber::MigrationHelpers
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,27 +3,16 @@
3
3
  module Rabarber
4
4
  module MigrationHelpers
5
5
  def migrate_authorization_context!(old_context, new_context)
6
- roles = Rabarber::Role.where(context_type: old_context.to_s)
6
+ old_context_type = Rabarber::Inputs::NonEmptyString.new(old_context).process
7
+ new_context_type = Rabarber::Inputs::NonEmptyString.new(new_context).process
7
8
 
8
- raise Rabarber::InvalidArgumentError, "No roles exist in context #{old_context.inspect}" unless roles.exists?
9
- raise Rabarber::InvalidArgumentError, "Cannot migrate context to #{new_context.inspect}: class does not exist" unless new_context.to_s.safe_constantize
10
-
11
- roles.update_all(context_type: new_context.to_s)
9
+ Rabarber::Role.where(context_type: old_context_type).update_all(context_type: new_context_type)
12
10
  end
13
11
 
14
12
  def delete_authorization_context!(context)
15
- roles = Rabarber::Role.where(context_type: context.to_s)
16
-
17
- raise Rabarber::InvalidArgumentError, "No roles exist in context #{context.inspect}" unless roles.exists?
13
+ context_type = Rabarber::Inputs::NonEmptyString.new(context).process
18
14
 
19
- ActiveRecord::Base.transaction do
20
- ActiveRecord::Base.connection.execute(
21
- ActiveRecord::Base.sanitize_sql(
22
- ["DELETE FROM rabarber_roles_roleables WHERE role_id IN (?)", roles.pluck(:id)]
23
- )
24
- )
25
- roles.delete_all
26
- end
15
+ Rabarber::Role.delete_with_assignments(Rabarber::Role.where(context_type:))
27
16
  end
28
17
  end
29
18
  end
@@ -3,7 +3,7 @@
3
3
  module Rabarber
4
4
  module Inputs
5
5
  class Base
6
- def initialize(value, optional: false, error: Rabarber::InvalidArgumentError, message: nil)
6
+ def initialize(value, optional: false, error: nil, message: nil)
7
7
  @value = value
8
8
  @optional = optional
9
9
  @error = error
@@ -22,8 +22,11 @@ module Rabarber
22
22
  raise NotImplementedError
23
23
  end
24
24
 
25
+ def default_error = Rabarber::InvalidArgumentError
26
+ def default_message = nil
27
+
25
28
  def raise_error
26
- raise @error, @message
29
+ raise @error || default_error, @message || default_message
27
30
  end
28
31
  end
29
32
  end
@@ -5,7 +5,9 @@ module Rabarber
5
5
  class Boolean < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
- def validate_and_normalize = @value == true || @value == false ? @value : raise_error
8
+ def default_message = "Expected a boolean, got `#{@value.inspect}`"
9
+
10
+ def validate_and_normalize = @value.equal?(true) || @value.equal?(false) ? @value : raise_error
9
11
  end
10
12
  end
11
13
  end
@@ -10,20 +10,19 @@ module Rabarber
10
10
  when Class
11
11
  { context_type: context.to_s, context_id: nil }
12
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
13
+ { context_type: context.class.to_s, context_id: context.id }
17
14
  end
18
15
  end
19
16
 
20
17
  private
21
18
 
19
+ def default_error = Rabarber::InvalidContextError
20
+ def default_message = "Expected an instance of ActiveRecord model, a Class, or nil, got `#{@value.inspect}`"
21
+
22
22
  def validate_and_normalize
23
23
  return @value if @value.nil?
24
24
  return @value if @value.is_a?(Class)
25
- return @value if @value.is_a?(ActiveRecord::Base)
26
- return @value if @value in { context_type: String | nil, context_id: String | Integer | nil }
25
+ return @value if @value.is_a?(ActiveRecord::Base) && @value.persisted?
27
26
 
28
27
  raise_error
29
28
  end
@@ -4,15 +4,10 @@ module Rabarber
4
4
  module Inputs
5
5
  module Contexts
6
6
  class Authorizational < Rabarber::Inputs::Context
7
- def resolve
8
- result = process
9
- return result if result.is_a?(Symbol) || result.is_a?(Proc)
10
-
11
- super
12
- end
13
-
14
7
  private
15
8
 
9
+ def default_message = "Expected a Class, an instance of ActiveRecord model, a symbol, a string, or a proc, got `#{@value.inspect}`"
10
+
16
11
  def validate_and_normalize
17
12
  return @value if @value.is_a?(Proc)
18
13
  return @value.to_sym if (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.present?
@@ -5,10 +5,12 @@ module Rabarber
5
5
  class DynamicRule < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol, a string, or a proc, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  return @value if @value.is_a?(Proc)
10
12
 
11
- Rabarber::Inputs::NonEmptySymbol.new(@value, error: @error, message: @message).process
13
+ Rabarber::Inputs::NonEmptySymbol.new(@value, error: @error || default_error, message: @message || default_message).process
12
14
  end
13
15
  end
14
16
  end
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class NonEmptyString < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a non-empty string, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  @value.is_a?(String) && @value.present? ? @value : raise_error
10
12
  end
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class NonEmptySymbol < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol or a string, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  raise_error unless (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.present?
10
12
 
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class Role < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  raise_error unless (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.to_s.match?(/\A[a-z0-9_]+\z/)
10
12
 
@@ -5,9 +5,11 @@ module Rabarber
5
5
  class Roles < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  Array(@value).map do |role|
10
- Rabarber::Inputs::Role.new(role, error: @error, message: @message).process
12
+ Rabarber::Inputs::Role.new(role, error: @error || default_error, message: @message || default_message).process
11
13
  end
12
14
  end
13
15
  end
@@ -9,113 +9,105 @@ module Rabarber
9
9
 
10
10
  @@included = name
11
11
 
12
- has_and_belongs_to_many :rabarber_roles, class_name: "Rabarber::Role",
13
- foreign_key: "roleable_id",
14
- join_table: "rabarber_roles_roleables"
12
+ has_many :rabarber_role_assignments, class_name: "Rabarber::RoleAssignment", foreign_key: "roleable_id", inverse_of: :roleable, dependent: :delete_all
13
+ has_many :rabarber_roles, through: :rabarber_role_assignments, source: :role
15
14
 
16
15
  scope :with_role, -> (*role_names, context: nil) {
17
16
  joins(:rabarber_roles).where(
18
- rabarber_roles: { name: process_role_names(role_names), **process_context(context) }
17
+ rabarber_roles: { name: Rabarber::Inputs::Roles.new(role_names).process, **Rabarber::Inputs::Context.new(context).resolve }
19
18
  ).distinct
20
19
  }
21
-
22
- class << self
23
- def process_role_names(role_names)
24
- Rabarber::Inputs::Roles.new(
25
- role_names,
26
- message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{role_names.inspect}"
27
- ).process
28
- end
29
-
30
- def process_context(context)
31
- Rabarber::Inputs::Context.new(
32
- context,
33
- error: Rabarber::InvalidContextError,
34
- message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
35
- ).resolve
36
- end
37
- end
38
- delegate :process_role_names, :process_context, to: :class, private: true
39
20
  end
40
21
 
41
22
  def roles(context: nil)
42
- processed_context = process_context(context)
43
- Rabarber::Core::Cache.fetch(roleable_id, processed_context) { rabarber_roles.list(context: processed_context) }
23
+ cached_roles(Rabarber::Inputs::Context.new(context).resolve)
44
24
  end
45
25
 
46
26
  def all_roles
47
- Rabarber::Core::Cache.fetch(roleable_id, :all) { rabarber_roles.list_all }
27
+ Rabarber::Core::Cache.fetch(id, :all) { assigned_roles.list_all }
48
28
  end
49
29
 
50
30
  def has_role?(*role_names, context: nil)
51
- processed_context = process_context(context)
52
- processed_roles = process_role_names(role_names)
53
- roles(context: processed_context).intersect?(processed_roles)
31
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
32
+ processed_roles = Rabarber::Inputs::Roles.new(role_names).process
33
+ cached_roles(processed_context).intersect?(processed_roles)
54
34
  end
55
35
 
56
36
  def assign_roles(*role_names, context: nil, create_new: true)
57
- processed_role_names = process_role_names(role_names)
58
- processed_context = process_context(context)
37
+ processed_create_new = Rabarber::Inputs::Boolean.new(create_new).process
38
+ warn_if_unsaved_roleable(:assign_roles)
59
39
 
60
- create_new_roles(processed_role_names, context: processed_context) if create_new
40
+ processed_role_names = Rabarber::Inputs::Roles.new(role_names).process
41
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
61
42
 
62
- roles_to_assign = Rabarber::Role.where(
63
- name: (processed_role_names - rabarber_roles.list(context: processed_context)), **processed_context
64
- )
43
+ Rabarber::Role.register_missing(processed_role_names, processed_context) if processed_create_new
44
+
45
+ role_ids = Rabarber::Role.where(
46
+ name: (processed_role_names - assigned_roles.names_in(processed_context)), **processed_context
47
+ ).ids
65
48
 
66
- if roles_to_assign.any?
67
- begin
68
- rabarber_roles << roles_to_assign
69
- rescue ActiveRecord::RecordNotUnique
49
+ if role_ids.any?
50
+ if persisted?
51
+ rabarber_role_assignments.insert_all(role_ids.map { { role_id: _1 } })
70
52
  rabarber_roles.reset
53
+ else
54
+ rabarber_roles << Rabarber::Role.where(id: role_ids)
71
55
  end
72
56
  delete_roleable_cache(contexts: [processed_context])
73
57
  end
74
58
 
75
- roles(context: processed_context)
59
+ cached_roles(processed_context)
76
60
  end
77
61
 
78
62
  def revoke_roles(*role_names, context: nil)
79
- processed_role_names = process_role_names(role_names)
80
- processed_context = process_context(context)
63
+ warn_if_unsaved_roleable(:revoke_roles)
81
64
 
82
- roles_to_revoke = Rabarber::Role.where(
83
- name: processed_role_names.intersection(rabarber_roles.list(context: processed_context)), **processed_context
84
- )
65
+ processed_role_names = Rabarber::Inputs::Roles.new(role_names).process
66
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
85
67
 
86
- if roles_to_revoke.any?
87
- self.rabarber_roles -= roles_to_revoke
68
+ if rabarber_role_assignments.where(role: Rabarber::Role.where(name: processed_role_names, **processed_context)).delete_all.positive?
69
+ rabarber_roles.reset
88
70
  delete_roleable_cache(contexts: [processed_context])
89
71
  end
90
72
 
91
- roles(context: processed_context)
73
+ cached_roles(processed_context)
92
74
  end
93
75
 
94
76
  def revoke_all_roles
95
- return [] if rabarber_roles.none?
77
+ warn_if_unsaved_roleable(:revoke_all_roles)
96
78
 
97
- contexts = all_roles.keys.map { process_context(_1) }
79
+ roles = assigned_roles.pluck(:id, :context_type, :context_id)
98
80
 
99
- rabarber_roles.clear
81
+ rabarber_role_assignments.where(role_id: roles.map(&:first)).delete_all
82
+ rabarber_role_assignments.reset
83
+ rabarber_roles.reset
100
84
 
101
- delete_roleable_cache(contexts:)
85
+ delete_roleable_cache(contexts: roles.map { |_, context_type, context_id| { context_type:, context_id: } }.uniq) if roles.any?
102
86
 
103
87
  []
104
88
  end
105
89
 
106
90
  private
107
91
 
108
- def create_new_roles(role_names, context:)
109
- new_roles = role_names - Rabarber.roles(context:)
110
- new_roles.each { |role_name| Rabarber::Role.register(role_name, context:) }
92
+ def warn_if_unsaved_roleable(method_name)
93
+ return unless new_record?
94
+
95
+ Rabarber.deprecator.warn(
96
+ "Calling ##{method_name} on an unsaved record is deprecated. Save the record before managing roles.",
97
+ caller_locations(2)
98
+ )
111
99
  end
112
100
 
113
- def delete_roleable_cache(contexts:)
114
- Rabarber::Core::Cache.delete(*contexts.map { [roleable_id, _1] }, [roleable_id, :all])
101
+ def assigned_roles
102
+ Rabarber::Role.where(id: rabarber_role_assignments.select(:role_id))
115
103
  end
116
104
 
117
- def roleable_id
118
- public_send(self.class.primary_key)
105
+ def cached_roles(processed_context)
106
+ Rabarber::Core::Cache.fetch(id, processed_context) { assigned_roles.names_in(processed_context) }
107
+ end
108
+
109
+ def delete_roleable_cache(contexts:)
110
+ Rabarber::Core::Cache.delete(*contexts.map { [id, _1] }, [id, :all])
119
111
  end
120
112
  end
121
113
  end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module/deprecation"
4
+
3
5
  module Rabarber
4
6
  module RoleManagement
5
7
  def roles(context: nil) = Rabarber::Role.list(context:)
6
8
  def all_roles = Rabarber::Role.list_all
7
- def create_role(name, context: nil) = Rabarber::Role.register(name, context:)
9
+ def create_role(name, context: nil) = Rabarber::Role.deprecated_register(name, context:)
10
+ def create_roles(*names, context: nil) = Rabarber::Role.register(names, context:)
8
11
  def rename_role(old_name, new_name, context: nil, force: false) = Rabarber::Role.amend(old_name, new_name, context:, force:)
9
- def delete_role(name, context: nil, force: false) = Rabarber::Role.drop(name, context:, force:)
12
+ def delete_role(name, context: nil, force: false) = Rabarber::Role.deprecated_drop(name, context:, force:)
13
+ def delete_roles(*names, context: nil, force: false) = Rabarber::Role.drop(names, context:, force:)
10
14
  def prune = Rabarber::Role.prune # rubocop:disable Rails/Delegate
15
+
16
+ deprecate create_role: :create_roles, delete_role: :delete_roles, deprecator: Rabarber.deprecator
11
17
  end
12
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "6.0.2"
4
+ VERSION = "6.1.0"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -25,6 +25,12 @@ module Rabarber
25
25
  class InvalidContextError < Rabarber::InvalidArgumentError; end
26
26
  class NotFoundError < Rabarber::Error; end
27
27
 
28
+ DEPRECATOR = ActiveSupport::Deprecation.new("7.0", "Rabarber")
29
+
30
+ def self.deprecator
31
+ DEPRECATOR
32
+ end
33
+
28
34
  delegate :configure, to: Rabarber::Configuration
29
35
  module_function :configure
30
36
  end
@@ -37,11 +43,10 @@ require_relative "rabarber/controllers/concerns/authorization"
37
43
  require_relative "rabarber/helpers/helpers"
38
44
  require_relative "rabarber/helpers/migration_helpers"
39
45
  require_relative "rabarber/models/concerns/roleable"
40
- require_relative "rabarber/models/role"
41
46
  require_relative "rabarber/models/role_management"
42
47
 
43
48
  Rabarber.extend Rabarber::RoleManagement
44
49
 
45
50
  require_relative "rabarber/core/permissions"
46
51
 
47
- require_relative "rabarber/railtie"
52
+ require_relative "rabarber/engine"
data/rabarber.gemspec CHANGED
@@ -7,14 +7,13 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Rabarber::VERSION
8
8
  spec.authors = ["enjaku4", "trafium"]
9
9
  spec.email = ["contact@brownbox.dev"]
10
- spec.homepage = "https://github.com/enjaku4/rabarber"
10
+ spec.homepage = "https://github.com/brownboxdev/rabarber-authorization"
11
11
  spec.metadata["homepage_uri"] = spec.homepage
12
12
  spec.metadata["source_code_uri"] = spec.homepage
13
13
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
14
14
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
15
15
  spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
16
16
  spec.metadata["mailing_list_uri"] = "#{spec.homepage}/discussions"
17
- spec.metadata["funding_uri"] = "https://github.com/sponsors/enjaku4"
18
17
  spec.metadata["rubygems_mfa_required"] = "true"
19
18
  spec.summary = "Simple role-based authorization library for Ruby on Rails"
20
19
  spec.description = "Simple role-based authorization for Rails applications with multi-tenancy support"
@@ -23,7 +22,7 @@ Gem::Specification.new do |spec|
23
22
 
24
23
  spec.files = [
25
24
  "rabarber.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"
26
- ] + Dir.glob("lib/**/*")
25
+ ] + Dir.glob("{app,lib}/**/*")
27
26
 
28
27
  spec.require_paths = ["lib"]
29
28
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rabarber
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.2
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
8
8
  - trafium
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-19 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -41,6 +41,8 @@ files:
41
41
  - CHANGELOG.md
42
42
  - LICENSE.txt
43
43
  - README.md
44
+ - app/models/rabarber/role.rb
45
+ - app/models/rabarber/role_assignment.rb
44
46
  - lib/generators/rabarber/roles_generator.rb
45
47
  - lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
46
48
  - lib/rabarber.rb
@@ -51,6 +53,7 @@ files:
51
53
  - lib/rabarber/core/permissions.rb
52
54
  - lib/rabarber/core/roleable.rb
53
55
  - lib/rabarber/core/rule.rb
56
+ - lib/rabarber/engine.rb
54
57
  - lib/rabarber/helpers/helpers.rb
55
58
  - lib/rabarber/helpers/migration_helpers.rb
56
59
  - lib/rabarber/inputs/base.rb
@@ -64,22 +67,19 @@ files:
64
67
  - lib/rabarber/inputs/role.rb
65
68
  - lib/rabarber/inputs/roles.rb
66
69
  - lib/rabarber/models/concerns/roleable.rb
67
- - lib/rabarber/models/role.rb
68
70
  - lib/rabarber/models/role_management.rb
69
- - lib/rabarber/railtie.rb
70
71
  - lib/rabarber/version.rb
71
72
  - rabarber.gemspec
72
- homepage: https://github.com/enjaku4/rabarber
73
+ homepage: https://github.com/brownboxdev/rabarber-authorization
73
74
  licenses:
74
75
  - MIT
75
76
  metadata:
76
- homepage_uri: https://github.com/enjaku4/rabarber
77
- source_code_uri: https://github.com/enjaku4/rabarber
78
- changelog_uri: https://github.com/enjaku4/rabarber/blob/main/CHANGELOG.md
79
- bug_tracker_uri: https://github.com/enjaku4/rabarber/issues
80
- documentation_uri: https://github.com/enjaku4/rabarber/blob/main/README.md
81
- mailing_list_uri: https://github.com/enjaku4/rabarber/discussions
82
- funding_uri: https://github.com/sponsors/enjaku4
77
+ homepage_uri: https://github.com/brownboxdev/rabarber-authorization
78
+ source_code_uri: https://github.com/brownboxdev/rabarber-authorization
79
+ changelog_uri: https://github.com/brownboxdev/rabarber-authorization/blob/main/CHANGELOG.md
80
+ bug_tracker_uri: https://github.com/brownboxdev/rabarber-authorization/issues
81
+ documentation_uri: https://github.com/brownboxdev/rabarber-authorization/blob/main/README.md
82
+ mailing_list_uri: https://github.com/brownboxdev/rabarber-authorization/discussions
83
83
  rubygems_mfa_required: 'true'
84
84
  rdoc_options: []
85
85
  require_paths:
@@ -98,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  - !ruby/object:Gem::Version
99
99
  version: '0'
100
100
  requirements: []
101
- rubygems_version: 4.0.15
101
+ rubygems_version: 4.0.16
102
102
  specification_version: 4
103
103
  summary: Simple role-based authorization library for Ruby on Rails
104
104
  test_files: []
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- class Role < ActiveRecord::Base
5
- self.table_name = "rabarber_roles"
6
-
7
- belongs_to :context, polymorphic: true, optional: true
8
-
9
- class << self
10
- def list(context: nil)
11
- where(process_context(context)).pluck(:name).map(&:to_sym)
12
- end
13
-
14
- def list_all
15
- includes(:context).each_with_object({}) do |role, hash|
16
- (hash[role.context] ||= []) << role.name.to_sym
17
- rescue ActiveRecord::RecordNotFound
18
- next
19
- end
20
- rescue NameError => e
21
- raise Rabarber::NotFoundError, "Context not found: class #{e.name} may have been renamed or deleted"
22
- end
23
-
24
- def register(name, context: nil)
25
- name = process_role_name(name)
26
- processed_context = process_context(context)
27
-
28
- return false if exists?(name:, **processed_context)
29
-
30
- !!create!(name:, **processed_context)
31
- rescue ActiveRecord::RecordNotUnique
32
- false
33
- end
34
-
35
- def amend(old_name, new_name, context: nil, force: false)
36
- processed_context = process_context(context)
37
- role = find_by(name: process_role_name(old_name), **processed_context)
38
-
39
- raise Rabarber::NotFoundError, "Role not found" unless role
40
-
41
- name = process_role_name(new_name)
42
-
43
- return false if exists?(name:, **processed_context) || role.roleables.exists? && !force
44
-
45
- role.update!(name:)
46
-
47
- delete_roleables_cache(role.roleables.pluck(:id), context: processed_context)
48
-
49
- true
50
- rescue ActiveRecord::RecordNotUnique
51
- false
52
- end
53
-
54
- def drop(name, context: nil, force: false)
55
- processed_context = process_context(context)
56
- role = find_by(name: process_role_name(name), **processed_context)
57
-
58
- raise Rabarber::NotFoundError, "Role not found" unless role
59
-
60
- return false if role.roleables.exists? && !force
61
-
62
- roleable_ids = role.roleables.pluck(:id)
63
-
64
- role.destroy!
65
-
66
- delete_roleables_cache(roleable_ids, context: processed_context)
67
-
68
- true
69
- end
70
-
71
- def prune
72
- orphaned_roles = where.not(context_id: nil).includes(:context).filter_map do |role|
73
- !role.context
74
- rescue ActiveRecord::RecordNotFound
75
- role.id
76
- end
77
-
78
- return if orphaned_roles.empty?
79
-
80
- ActiveRecord::Base.transaction do
81
- ActiveRecord::Base.connection.execute(
82
- ActiveRecord::Base.sanitize_sql(
83
- ["DELETE FROM rabarber_roles_roleables WHERE role_id IN (?)", orphaned_roles]
84
- )
85
- )
86
- where(id: orphaned_roles).delete_all
87
- end
88
-
89
- Rabarber::Core::Cache.clear
90
- end
91
-
92
- private
93
-
94
- def delete_roleables_cache(roleable_ids, context:)
95
- Rabarber::Core::Cache.delete(*roleable_ids.flat_map { [[_1, context], [_1, :all]] })
96
- end
97
-
98
- def process_role_name(name)
99
- Rabarber::Inputs::Role.new(
100
- name,
101
- message: "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got #{name.inspect}"
102
- ).process
103
- end
104
-
105
- def process_context(context)
106
- Rabarber::Inputs::Context.new(
107
- context,
108
- error: Rabarber::InvalidContextError,
109
- message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
110
- ).resolve
111
- end
112
- end
113
-
114
- def context
115
- return context_type.constantize if context_type.present? && context_id.blank?
116
-
117
- record = super
118
-
119
- raise ActiveRecord::RecordNotFound.new(nil, context_type, nil, context_id) if context_id.present? && !record
120
-
121
- record
122
- end
123
- end
124
- end