rabarber 5.0.0 → 5.1.1

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.
@@ -1,36 +1,54 @@
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.process(
16
+ value,
17
+ as: :boolean,
18
+ error: Rabarber::ConfigurationError,
19
+ message: "Invalid configuration `cache_enabled`, expected a boolean, got #{value.inspect}"
20
+ )
21
+ end
22
+ setting :current_user_method,
23
+ default: :current_user,
24
+ reader: true,
25
+ constructor: -> (value) do
26
+ Rabarber::Inputs.process(
27
+ value,
28
+ as: :symbol,
29
+ error: Rabarber::ConfigurationError,
30
+ message: "Invalid configuration `current_user_method`, expected a symbol or a string, got #{value.inspect}"
31
+ )
32
+ end
33
+ setting :user_model_name,
34
+ default: "User",
35
+ reader: true,
36
+ constructor: -> (value) do
37
+ Rabarber::Inputs.process(
38
+ value,
39
+ as: :string,
40
+ error: Rabarber::ConfigurationError,
41
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{value.inspect}"
42
+ )
43
+ end
29
44
 
30
45
  def user_model
31
- Rabarber::Input::ArModel.new(
32
- @user_model_name, Rabarber::ConfigurationError, "Configuration `user_model_name` must be an ActiveRecord model name"
33
- ).process
46
+ Rabarber::Inputs.process(
47
+ user_model_name,
48
+ as: :model,
49
+ error: Rabarber::ConfigurationError,
50
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{user_model_name.inspect}"
51
+ )
34
52
  end
35
53
  end
36
54
  end
@@ -9,23 +9,46 @@ module Rabarber
9
9
  def with_authorization(options = {})
10
10
  before_action :with_authorization, **options
11
11
  rescue ArgumentError => e
12
- raise Rabarber::Error, e.message
12
+ raise Rabarber::InvalidArgumentError, e.message
13
13
  end
14
14
 
15
15
  def skip_authorization(options = {})
16
16
  skip_before_action :with_authorization, **options
17
17
  rescue ArgumentError => e
18
- raise Rabarber::Error, e.message
18
+ raise Rabarber::InvalidArgumentError, e.message
19
19
  end
20
20
 
21
21
  def grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)
22
22
  Rabarber::Core::Permissions.add(
23
23
  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
24
+ Rabarber::Inputs.process(
25
+ action,
26
+ as: :symbol,
27
+ optional: true,
28
+ message: "Expected a symbol or a string, got #{action.inspect}"
29
+ ),
30
+ Rabarber::Inputs.process(
31
+ roles,
32
+ as: :roles,
33
+ message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{roles.inspect}"
34
+ ),
35
+ Rabarber::Inputs.process(
36
+ context,
37
+ as: :authorization_context,
38
+ message: "Expected a Class, an instance of ActiveRecord model, a symbol, a string, or a proc, got #{context.inspect}"
39
+ ),
40
+ Rabarber::Inputs.process(
41
+ binding.local_variable_get(:if),
42
+ as: :dynamic_rule,
43
+ optional: true,
44
+ message: "Expected a symbol, a string, or a proc, got #{binding.local_variable_get(:if).inspect}"
45
+ ),
46
+ Rabarber::Inputs.process(
47
+ binding.local_variable_get(:unless),
48
+ as: :dynamic_rule,
49
+ optional: true,
50
+ message: "Expected a symbol, a string, or a proc, got #{binding.local_variable_get(:unless).inspect}"
51
+ )
29
52
  )
30
53
  end
31
54
  end
@@ -39,7 +62,7 @@ module Rabarber
39
62
  end
40
63
 
41
64
  def when_unauthorized
42
- request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
65
+ request.format.html? ? redirect_back(fallback_location: root_path) : head(:forbidden)
43
66
  end
44
67
  end
45
68
  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
@@ -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
 
@@ -35,11 +35,12 @@ module Rabarber
35
35
  end
36
36
 
37
37
  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
38
+ resolved_context = case context
39
+ when Proc then controller_instance.instance_exec(&context)
40
+ when Symbol then controller_instance.send(context)
41
+ else context
42
+ end
43
+ Rabarber::Inputs.process(resolved_context, as: :role_context, message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{resolved_context.inspect}")
43
44
  end
44
45
  end
45
46
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+
5
+ module Rabarber
6
+ module Inputs
7
+ extend self
8
+
9
+ include Dry.Types()
10
+
11
+ PROC_TYPE = self::Instance(Proc)
12
+ SYMBOL_TYPE = self::Coercible::Symbol.constrained(min_size: 1)
13
+ ROLE_TYPE = self::SYMBOL_TYPE.constrained(format: /\A[a-z0-9_]+\z/)
14
+
15
+ CONTEXT_TYPE = self::Strict::Class | self::Instance(ActiveRecord::Base) | self::Hash.schema(
16
+ context_type: self::Strict::String | self::Nil,
17
+ context_id: self::Strict::String | self::Strict::Integer | self::Nil
18
+ ) | self::Nil
19
+
20
+ TYPES = {
21
+ boolean: self::Strict::Bool,
22
+ string: self::Strict::String.constrained(min_size: 1).constructor { _1.is_a?(::String) ? _1.strip : _1 },
23
+ symbol: self::SYMBOL_TYPE,
24
+ role: self::ROLE_TYPE,
25
+ roles: self::Array.of(self::ROLE_TYPE).constructor { Kernel::Array(_1) },
26
+ model: self::Strict::Class.constructor { _1.try(:safe_constantize) }.constrained(lt: ActiveRecord::Base),
27
+ dynamic_rule: self::SYMBOL_TYPE | self::PROC_TYPE,
28
+ role_context: self::CONTEXT_TYPE,
29
+ authorization_context: self::SYMBOL_TYPE | self::PROC_TYPE | self::CONTEXT_TYPE
30
+ }.freeze
31
+
32
+ def process(value, as:, optional: false, error: Rabarber::InvalidArgumentError, message: nil)
33
+ checker = type_for(as)
34
+ checker = checker.optional if optional
35
+
36
+ result = checker[value]
37
+
38
+ [:role_context, :authorization_context].include?(as) ? resolve_context(result) : result
39
+ rescue Dry::Types::CoercionError => e
40
+ raise error, message || e.message
41
+ end
42
+
43
+ private
44
+
45
+ def type_for(name) = self::TYPES.fetch(name)
46
+
47
+ def resolve_context(value)
48
+ case value
49
+ when nil
50
+ { context_type: nil, context_id: nil }
51
+ when Class
52
+ { context_type: value.to_s, context_id: nil }
53
+ when ActiveRecord::Base
54
+ raise Dry::Types::CoercionError, "instance context not persisted" unless value.persisted?
55
+
56
+ { context_type: value.class.to_s, context_id: value.public_send(value.class.primary_key) }
57
+ else
58
+ value
59
+ end
60
+ end
61
+ end
62
+ end
@@ -40,7 +40,7 @@ module Rabarber
40
40
  )
41
41
 
42
42
  if roles_to_assign.any?
43
- delete_roleable_cache(context: processed_context)
43
+ delete_roleable_cache(contexts: [processed_context])
44
44
  rabarber_roles << roles_to_assign
45
45
  end
46
46
 
@@ -56,13 +56,23 @@ module Rabarber
56
56
  )
57
57
 
58
58
  if roles_to_revoke.any?
59
- delete_roleable_cache(context: processed_context)
59
+ delete_roleable_cache(contexts: [processed_context])
60
60
  self.rabarber_roles -= roles_to_revoke
61
61
  end
62
62
 
63
63
  roles(context: processed_context)
64
64
  end
65
65
 
66
+ def revoke_all_roles
67
+ return if rabarber_roles.none?
68
+
69
+ contexts = all_roles.keys.map { process_context(_1) }
70
+
71
+ rabarber_roles.clear
72
+
73
+ delete_roleable_cache(contexts:)
74
+ end
75
+
66
76
  private
67
77
 
68
78
  def create_new_roles(role_names, context:)
@@ -71,15 +81,23 @@ module Rabarber
71
81
  end
72
82
 
73
83
  def process_role_names(role_names)
74
- Rabarber::Input::Roles.new(role_names).process
84
+ Rabarber::Inputs.process(
85
+ role_names,
86
+ as: :roles,
87
+ message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{role_names.inspect}"
88
+ )
75
89
  end
76
90
 
77
91
  def process_context(context)
78
- Rabarber::Input::Context.new(context).process
92
+ Rabarber::Inputs.process(
93
+ context,
94
+ as: :role_context,
95
+ message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
96
+ )
79
97
  end
80
98
 
81
- def delete_roleable_cache(context:)
82
- Rabarber::Core::Cache.delete([roleable_id, context], [roleable_id, :all])
99
+ def delete_roleable_cache(contexts:)
100
+ Rabarber::Core::Cache.delete(*contexts.map { [roleable_id, _1] }, [roleable_id, :all])
83
101
  end
84
102
 
85
103
  def roleable_id
@@ -4,14 +4,9 @@ module Rabarber
4
4
  class Role < ActiveRecord::Base
5
5
  self.table_name = "rabarber_roles"
6
6
 
7
- validates :name, presence: true,
8
- uniqueness: { scope: [:context_type, :context_id] },
9
- format: { with: Rabarber::Input::Role::REGEX },
10
- strict: true
11
-
12
7
  belongs_to :context, polymorphic: true, optional: true
13
8
 
14
- 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,
15
10
  association_foreign_key: "roleable_id",
16
11
  join_table: "rabarber_roles_roleables"
17
12
 
@@ -68,8 +63,7 @@ module Rabarber
68
63
  end
69
64
 
70
65
  def assignees(name, context: nil)
71
- find_by(name: process_role_name(name), **process_context(context))&.roleables ||
72
- Rabarber::Configuration.instance.user_model.none
66
+ find_by(name: process_role_name(name), **process_context(context))&.roleables || Rabarber::Configuration.user_model.none
73
67
  end
74
68
 
75
69
  private
@@ -81,11 +75,19 @@ module Rabarber
81
75
  end
82
76
 
83
77
  def process_role_name(name)
84
- Rabarber::Input::Role.new(name).process
78
+ Rabarber::Inputs.process(
79
+ name,
80
+ as: :role,
81
+ message: "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got #{name.inspect}"
82
+ )
85
83
  end
86
84
 
87
85
  def process_context(context)
88
- Rabarber::Input::Context.new(context).process
86
+ Rabarber::Inputs.process(
87
+ context,
88
+ as: :role_context,
89
+ message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
90
+ )
89
91
  end
90
92
  end
91
93
 
@@ -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.0.0"
4
+ VERSION = "5.1.1"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -1,22 +1,23 @@
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"
9
+
10
+ require_relative "rabarber/configuration"
11
+
12
+ module Rabarber
13
+ class Error < StandardError; end
14
+ class ConfigurationError < Rabarber::Error; end
15
+ class InvalidArgumentError < Rabarber::Error; end
16
+ class NotFoundError < Rabarber::Error; end
17
+
18
+ delegate :configure, to: Rabarber::Configuration
19
+ module_function :configure
20
+ end
20
21
 
21
22
  require_relative "rabarber/core/cache"
22
23
 
@@ -32,15 +33,3 @@ require_relative "rabarber/core/permissions"
32
33
  require_relative "rabarber/core/integrity_checker"
33
34
 
34
35
  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.3"
25
+ spec.add_dependency "dry-types", "~> 1.8"
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.0.0
4
+ version: 5.1.1
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.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
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.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: rails
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -50,17 +78,7 @@ 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.rb
64
82
  - lib/rabarber/models/concerns/has_roles.rb
65
83
  - lib/rabarber/models/role.rb
66
84
  - 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