action_policy 0.6.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +5 -3
  4. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +8 -2
  5. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +25 -29
  6. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +70 -0
  7. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +1 -1
  8. data/lib/.rbnext/3.0/action_policy/policy/core.rb +1 -1
  9. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +8 -2
  10. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +25 -29
  11. data/lib/.rbnext/3.1/action_policy/behaviours/policy_for.rb +5 -3
  12. data/lib/.rbnext/3.1/action_policy/policy/authorization.rb +1 -0
  13. data/lib/.rbnext/3.2/action_policy/behaviours/policy_for.rb +5 -3
  14. data/lib/.rbnext/3.2/action_policy/policy/core.rb +1 -1
  15. data/lib/.rbnext/3.2/action_policy/rspec/have_authorized_scope.rb +8 -2
  16. data/lib/action_policy/behaviours/memoized.rb +1 -1
  17. data/lib/action_policy/behaviours/policy_for.rb +5 -3
  18. data/lib/action_policy/behaviours/thread_memoized.rb +1 -1
  19. data/lib/action_policy/policy/authorization.rb +1 -0
  20. data/lib/action_policy/policy/core.rb +1 -1
  21. data/lib/action_policy/policy/scoping.rb +12 -1
  22. data/lib/action_policy/rails/controller.rb +1 -1
  23. data/lib/action_policy/rspec/have_authorized_scope.rb +8 -2
  24. data/lib/action_policy/test_helper.rb +5 -2
  25. data/lib/action_policy/testing.rb +17 -10
  26. data/lib/action_policy/utils/pretty_print.rb +25 -29
  27. data/lib/action_policy/version.rb +1 -1
  28. metadata +8 -8
  29. data/lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb +0 -159
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e78f24a8faa881e8cad62f6bb4fc58921668fb7d41ac933c7fb1934117acc7d
4
- data.tar.gz: 39f55de2e98b9a1d67519df40452349a2007e8d7168039438d2c8469cc7779e7
3
+ metadata.gz: 0e9a1595099a6b87f22ec08fbd6676cd73cea94672c8cc9276525e82e9ba8c1a
4
+ data.tar.gz: bc743bbc45679d6583d3549cad77fc37f4004d051ef54d4cf8baa252730b0621
5
5
  SHA512:
6
- metadata.gz: 66b0de36683bc63ce349b0fba097585b7fcb4d7d65145da53c26cd5d6f0c798ef76c447d75a987c1f16110d3d4bb379153407ef79b531613cc6cfa8e55171570
7
- data.tar.gz: b7eadbbd0020e60addce888bf8287722c8bafd32a8b35ff2c66d8ea51eaa3b9c6f406e5214fdcddda4e7d4d527d12f08be3f86eac64d28db3b62b7976de28f17
6
+ metadata.gz: cf8db058edbeb5b198fb58f8ccab0d35e1f71aeb7891a6bab93bf6e04b9819fc321706bfbbe6437b58021325f01f62d5e891e812690a80f8464ef2c825ef8e47
7
+ data.tar.gz: 8aceb6e4a30d97e487b7bb29de4393c3c7f8598261c12f36dd66743d2a3c469bce61423c062e38b757d5265711d07172a743d04060dc336521f2dd10f80b7693
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.0 (2024-06-20)
6
+
7
+ - **Ruby 2.7+** is required.
8
+
9
+ - Support using callable objects as scopes. ([@killondark][])
10
+
11
+ ## 0.6.9 (2024-04-19)
12
+
13
+ - Add `.with_context` modifier to the `#have_authorized_scope` matcher. ([@killondark][])
14
+
5
15
  ## 0.6.8 (2024-01-17)
6
16
 
7
17
  - Do not preload Rails base classes, use load hooks everywhere. ([@palkan][])
@@ -509,3 +519,4 @@ This value is now stored in a cache (if any) instead of just the call result (`t
509
519
  [@skojin]: https://github.com/skojin
510
520
  [@tomdalling]: https://github.com/tomdalling
511
521
  [@matsales28]: https://github.com/matsales28
522
+ [@killondark]: https://github.com/killondark
@@ -9,7 +9,7 @@ module ActionPolicy
9
9
 
10
10
  # Returns policy instance for the record.
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
- context = context ? authorization_context.merge(context) : authorization_context
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
13
 
14
14
  policy_class = with || ::ActionPolicy.lookup(
15
15
  record,
@@ -18,8 +18,10 @@ module ActionPolicy
18
18
  policy_class&.new(record, **context)
19
19
  end
20
20
 
21
- def authorization_context
22
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
+ def authorization_context ; @authorization_context ||= build_authorization_context; end
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
23
25
  end
24
26
 
25
27
  def authorization_namespace
@@ -21,7 +21,7 @@ module ActionPolicy
21
21
  #
22
22
  class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
23
  attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations
24
+ :target_expectations, :context
25
25
 
26
26
  def initialize(type)
27
27
  @type = type
@@ -49,6 +49,11 @@ module ActionPolicy
49
49
  self
50
50
  end
51
51
 
52
+ def with_context(context)
53
+ @context = context
54
+ self
55
+ end
56
+
52
57
  def match(_expected, actual)
53
58
  raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
54
59
 
@@ -56,7 +61,7 @@ module ActionPolicy
56
61
 
57
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
63
 
59
- matching_scopes = actual_scopes.select { |_1| _1.matches?(policy, type, name, scope_options) }
64
+ matching_scopes = actual_scopes.select { |_1| _1.matches?(policy, type, name, scope_options, context) }
60
65
 
61
66
  return false if matching_scopes.empty?
62
67
 
@@ -80,6 +85,7 @@ module ActionPolicy
80
85
  def failure_message
81
86
  "expected a scoping named :#{name} for type :#{type} " \
82
87
  "#{scope_options_message} " \
88
+ "#{context ? "and context #{context.inspect} " : ""}" \
83
89
  "from #{policy} to have been applied, " \
84
90
  "but #{actual_scopes_message}"
85
91
  end
@@ -7,8 +7,7 @@ begin
7
7
  # Ignore parse warnings when patch
8
8
  # Ruby version mismatches
9
9
  $VERBOSE = nil
10
- require "parser/current"
11
- require "unparser"
10
+ require "prism"
12
11
  rescue LoadError
13
12
  # do nothing
14
13
  ensure
@@ -42,7 +41,7 @@ module ActionPolicy
42
41
  FALSE = "\e[31mfalse\e[0m"
43
42
 
44
43
  class Visitor
45
- attr_reader :lines, :object
44
+ attr_reader :lines, :object, :source
46
45
  attr_accessor :indent
47
46
 
48
47
  def initialize(object)
@@ -52,6 +51,8 @@ module ActionPolicy
52
51
  def collect(ast)
53
52
  @lines = []
54
53
  @indent = 0
54
+ @source = ast.source.source
55
+ ast = ast.value.child_nodes[0].child_nodes[0].body
55
56
 
56
57
  visit_node(ast)
57
58
 
@@ -67,7 +68,7 @@ module ActionPolicy
67
68
  end
68
69
 
69
70
  def expression_with_result(sexp)
70
- expression = Unparser.unparse(sexp)
71
+ expression = source[sexp.location.start_offset...sexp.location.end_offset]
71
72
  "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
73
  end
73
74
 
@@ -78,36 +79,33 @@ module ActionPolicy
78
79
  "Failed: #{e.message}"
79
80
  end
80
81
 
81
- def visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
93
- def visit_begin(ast)
94
- # Parens
95
- if ast.children.size == 1
96
- lines << indented("(")
97
- self.indent += 2
98
- visit_node(ast.children[0])
94
+ def visit_statements_node(ast)
95
+ ast.child_nodes.each do |node|
96
+ visit_node(node)
97
+ # restore indent after each expression
99
98
  self.indent -= 2
100
- lines << indented(")")
101
- else
102
- # Multiple expressions
103
- ast.children.each do |node|
104
- visit_node(node)
105
- # restore indent after each expression
106
- self.indent -= 2
107
- end
108
99
  end
109
100
  end
110
101
 
102
+ def visit_parentheses_node(ast)
103
+ lines << indented("(")
104
+ self.indent += 2
105
+ visit_node(ast.child_nodes[0])
106
+ lines << indented(")")
107
+ end
108
+
111
109
  def visit_missing(ast)
112
110
  lines << indented(expression_with_result(ast))
113
111
  end
@@ -128,15 +126,13 @@ module ActionPolicy
128
126
  class << self
129
127
  attr_accessor :ignore_expressions
130
128
 
131
- if defined?(::Unparser) && defined?(::MethodSource)
129
+ if defined?(::Prism) && defined?(::MethodSource)
132
130
  def available?() ; true; end
133
131
 
134
132
  def print_method(object, method_name)
135
- ast = object.method(method_name).source.then(&Unparser.method(:parse))
136
- # outer node is a method definition itself
137
- body = ast.children[2]
133
+ ast = Prism.parse(object.method(method_name).source)
138
134
 
139
- Visitor.new(object).collect(body)
135
+ Visitor.new(object).collect(ast)
140
136
  end
141
137
  else
142
138
  def available?() ; false; end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Adds `policy_for` method
6
+ module PolicyFor
7
+ require "action_policy/ext/policy_cache_key"
8
+ using ActionPolicy::Ext::PolicyCacheKey
9
+
10
+ # Returns policy instance for the record.
11
+ def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
+
14
+ policy_class = with || ::ActionPolicy.lookup(
15
+ record,
16
+ namespace: namespace, context: context, allow_nil: allow_nil, default: default, strict_namespace: strict_namespace
17
+ )
18
+ policy_class&.new(record, **context)
19
+ end
20
+
21
+ def authorization_context ; @authorization_context ||= build_authorization_context; end
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
25
+ end
26
+
27
+ def authorization_namespace
28
+ # override to provide specific authorization namespace
29
+ end
30
+
31
+ def default_authorization_policy_class
32
+ # override to provide a policy class use when no policy found
33
+ end
34
+
35
+ def authorization_strict_namespace
36
+ # override to provide strict namespace lookup option
37
+ end
38
+
39
+ # Override this method to provide implicit authorization target
40
+ # that would be used in case `record` is not specified in
41
+ # `authorize!` and `allowed_to?` call.
42
+ #
43
+ # It is also used to infer a policy for scoping (in `authorized_scope` method).
44
+ def implicit_authorization_target
45
+ # no-op
46
+ end
47
+
48
+ # Return implicit authorization target or raises an exception if it's nil
49
+ def implicit_authorization_target!
50
+ implicit_authorization_target || Kernel.raise(
51
+ NotFound,
52
+ [
53
+ self,
54
+ "Couldn't find implicit authorization target " \
55
+ "for #{self.class}. " \
56
+ "Please, provide policy class explicitly using `with` option or " \
57
+ "define the `implicit_authorization_target` method."
58
+ ]
59
+ )
60
+ end
61
+
62
+ def policy_for_cache_key(record:, with: nil, namespace: nil, context: authorization_context, **__kwrest__)
63
+ record_key = record._policy_cache_key(use_object_id: true)
64
+ context_key = context.values.map { _1._policy_cache_key(use_object_id: true) }.join(".")
65
+
66
+ "#{namespace}/#{with}/#{context_key}/#{record_key}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  module InstanceMethods # :nodoc:
47
47
  def policy_for(record:, **opts)
48
- __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
48
+ __policy_thread_memoize__(record, **opts) { super }
49
49
  end
50
50
  end
51
51
 
@@ -147,7 +147,7 @@ module ActionPolicy
147
147
  end
148
148
 
149
149
  # Return annotated source code for the rule
150
- # NOTE: require "method_source" and "unparser" gems to be installed.
150
+ # NOTE: require "method_source" and "prism" gems to be installed.
151
151
  # Otherwise returns empty string.
152
152
  def inspect_rule(rule) ; PrettyPrint.print_method(self, rule); end
153
153
 
@@ -21,7 +21,7 @@ module ActionPolicy
21
21
  #
22
22
  class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
23
  attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations
24
+ :target_expectations, :context
25
25
 
26
26
  def initialize(type)
27
27
  @type = type
@@ -49,6 +49,11 @@ module ActionPolicy
49
49
  self
50
50
  end
51
51
 
52
+ def with_context(context)
53
+ @context = context
54
+ self
55
+ end
56
+
52
57
  def match(_expected, actual)
53
58
  raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
54
59
 
@@ -56,7 +61,7 @@ module ActionPolicy
56
61
 
57
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
63
 
59
- matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
64
+ matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
60
65
 
61
66
  return false if matching_scopes.empty?
62
67
 
@@ -80,6 +85,7 @@ module ActionPolicy
80
85
  def failure_message
81
86
  "expected a scoping named :#{name} for type :#{type} " \
82
87
  "#{scope_options_message} " \
88
+ "#{context ? "and context #{context.inspect} " : ""}" \
83
89
  "from #{policy} to have been applied, " \
84
90
  "but #{actual_scopes_message}"
85
91
  end
@@ -7,8 +7,7 @@ begin
7
7
  # Ignore parse warnings when patch
8
8
  # Ruby version mismatches
9
9
  $VERBOSE = nil
10
- require "parser/current"
11
- require "unparser"
10
+ require "prism"
12
11
  rescue LoadError
13
12
  # do nothing
14
13
  ensure
@@ -42,7 +41,7 @@ module ActionPolicy
42
41
  FALSE = "\e[31mfalse\e[0m"
43
42
 
44
43
  class Visitor
45
- attr_reader :lines, :object
44
+ attr_reader :lines, :object, :source
46
45
  attr_accessor :indent
47
46
 
48
47
  def initialize(object)
@@ -52,6 +51,8 @@ module ActionPolicy
52
51
  def collect(ast)
53
52
  @lines = []
54
53
  @indent = 0
54
+ @source = ast.source.source
55
+ ast = ast.value.child_nodes[0].child_nodes[0].body
55
56
 
56
57
  visit_node(ast)
57
58
 
@@ -67,7 +68,7 @@ module ActionPolicy
67
68
  end
68
69
 
69
70
  def expression_with_result(sexp)
70
- expression = Unparser.unparse(sexp)
71
+ expression = source[sexp.location.start_offset...sexp.location.end_offset]
71
72
  "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
73
  end
73
74
 
@@ -78,36 +79,33 @@ module ActionPolicy
78
79
  "Failed: #{e.message}"
79
80
  end
80
81
 
81
- def visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
93
- def visit_begin(ast)
94
- # Parens
95
- if ast.children.size == 1
96
- lines << indented("(")
97
- self.indent += 2
98
- visit_node(ast.children[0])
94
+ def visit_statements_node(ast)
95
+ ast.child_nodes.each do |node|
96
+ visit_node(node)
97
+ # restore indent after each expression
99
98
  self.indent -= 2
100
- lines << indented(")")
101
- else
102
- # Multiple expressions
103
- ast.children.each do |node|
104
- visit_node(node)
105
- # restore indent after each expression
106
- self.indent -= 2
107
- end
108
99
  end
109
100
  end
110
101
 
102
+ def visit_parentheses_node(ast)
103
+ lines << indented("(")
104
+ self.indent += 2
105
+ visit_node(ast.child_nodes[0])
106
+ lines << indented(")")
107
+ end
108
+
111
109
  def visit_missing(ast)
112
110
  lines << indented(expression_with_result(ast))
113
111
  end
@@ -128,15 +126,13 @@ module ActionPolicy
128
126
  class << self
129
127
  attr_accessor :ignore_expressions
130
128
 
131
- if defined?(::Unparser) && defined?(::MethodSource)
129
+ if defined?(::Prism) && defined?(::MethodSource)
132
130
  def available?() ; true; end
133
131
 
134
132
  def print_method(object, method_name)
135
- ast = object.method(method_name).source.then(&Unparser.method(:parse))
136
- # outer node is a method definition itself
137
- body = ast.children[2]
133
+ ast = Prism.parse(object.method(method_name).source)
138
134
 
139
- Visitor.new(object).collect(body)
135
+ Visitor.new(object).collect(ast)
140
136
  end
141
137
  else
142
138
  def available?() ; false; end
@@ -9,7 +9,7 @@ module ActionPolicy
9
9
 
10
10
  # Returns policy instance for the record.
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
- context = context ? authorization_context.merge(context) : authorization_context
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
13
 
14
14
  policy_class = with || ::ActionPolicy.lookup(
15
15
  record,
@@ -18,8 +18,10 @@ module ActionPolicy
18
18
  policy_class&.new(record, **context)
19
19
  end
20
20
 
21
- def authorization_context
22
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
+ def authorization_context = @authorization_context ||= build_authorization_context
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
23
25
  end
24
26
 
25
27
  def authorization_namespace
@@ -42,6 +42,7 @@ module ActionPolicy
42
42
  end
43
43
 
44
44
  attr_reader :authorization_context
45
+ alias_method :build_authorization_context, :authorization_context
45
46
 
46
47
  def initialize(record = nil, **params)
47
48
  super(record)
@@ -9,7 +9,7 @@ module ActionPolicy
9
9
 
10
10
  # Returns policy instance for the record.
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
- context = context ? authorization_context.merge(context) : authorization_context
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
13
 
14
14
  policy_class = with || ::ActionPolicy.lookup(
15
15
  record,
@@ -18,8 +18,10 @@ module ActionPolicy
18
18
  policy_class&.new(record, **context)
19
19
  end
20
20
 
21
- def authorization_context
22
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
+ def authorization_context = @authorization_context ||= build_authorization_context
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
23
25
  end
24
26
 
25
27
  def authorization_namespace
@@ -147,7 +147,7 @@ module ActionPolicy
147
147
  end
148
148
 
149
149
  # Return annotated source code for the rule
150
- # NOTE: require "method_source" and "unparser" gems to be installed.
150
+ # NOTE: require "method_source" and "prism" gems to be installed.
151
151
  # Otherwise returns empty string.
152
152
  def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
153
153
 
@@ -21,7 +21,7 @@ module ActionPolicy
21
21
  #
22
22
  class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
23
  attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations
24
+ :target_expectations, :context
25
25
 
26
26
  def initialize(type)
27
27
  @type = type
@@ -49,6 +49,11 @@ module ActionPolicy
49
49
  self
50
50
  end
51
51
 
52
+ def with_context(context)
53
+ @context = context
54
+ self
55
+ end
56
+
52
57
  def match(_expected, actual)
53
58
  raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
54
59
 
@@ -56,7 +61,7 @@ module ActionPolicy
56
61
 
57
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
63
 
59
- matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
64
+ matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
60
65
 
61
66
  return false if matching_scopes.empty?
62
67
 
@@ -80,6 +85,7 @@ module ActionPolicy
80
85
  def failure_message
81
86
  "expected a scoping named :#{name} for type :#{type} " \
82
87
  "#{scope_options_message} " \
88
+ "#{context ? "and context #{context.inspect} " : ""}" \
83
89
  "from #{policy} to have been applied, " \
84
90
  "but #{actual_scopes_message}"
85
91
  end
@@ -29,7 +29,7 @@ module ActionPolicy
29
29
 
30
30
  module InstanceMethods # :nodoc:
31
31
  def policy_for(record:, **opts)
32
- __policy_memoize__(record, **opts) { super(record: record, **opts) }
32
+ __policy_memoize__(record, **opts) { super }
33
33
  end
34
34
  end
35
35
 
@@ -9,7 +9,7 @@ module ActionPolicy
9
9
 
10
10
  # Returns policy instance for the record.
11
11
  def policy_for(record:, with: nil, namespace: authorization_namespace, context: nil, allow_nil: false, default: default_authorization_policy_class, strict_namespace: authorization_strict_namespace)
12
- context = context ? authorization_context.merge(context) : authorization_context
12
+ context = context ? build_authorization_context.merge(context) : authorization_context
13
13
 
14
14
  policy_class = with || ::ActionPolicy.lookup(
15
15
  record,
@@ -18,8 +18,10 @@ module ActionPolicy
18
18
  policy_class&.new(record, **context)
19
19
  end
20
20
 
21
- def authorization_context
22
- Kernel.raise NotImplementedError, "Please, define `authorization_context` method!"
21
+ def authorization_context = @authorization_context ||= build_authorization_context
22
+
23
+ def build_authorization_context
24
+ Kernel.raise NotImplementedError, "Please, define `build_authorization_context` method!"
23
25
  end
24
26
 
25
27
  def authorization_namespace
@@ -45,7 +45,7 @@ module ActionPolicy
45
45
 
46
46
  module InstanceMethods # :nodoc:
47
47
  def policy_for(record:, **opts)
48
- __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
48
+ __policy_thread_memoize__(record, **opts) { super }
49
49
  end
50
50
  end
51
51
 
@@ -42,6 +42,7 @@ module ActionPolicy
42
42
  end
43
43
 
44
44
  attr_reader :authorization_context
45
+ alias_method :build_authorization_context, :authorization_context
45
46
 
46
47
  def initialize(record = nil, **params)
47
48
  super(record)
@@ -147,7 +147,7 @@ module ActionPolicy
147
147
  end
148
148
 
149
149
  # Return annotated source code for the rule
150
- # NOTE: require "method_source" and "unparser" gems to be installed.
150
+ # NOTE: require "method_source" and "prism" gems to be installed.
151
151
  # Otherwise returns empty string.
152
152
  def inspect_rule(rule) = PrettyPrint.print_method(self, rule)
153
153
 
@@ -113,8 +113,11 @@ module ActionPolicy
113
113
 
114
114
  module ClassMethods # :nodoc:
115
115
  # Register a new scoping method for the `type`
116
- def scope_for(type, name = :default, &block)
116
+ def scope_for(type, name = :default, callable = nil, &block)
117
+ name, callable = prepare_args(name, callable)
118
+
117
119
  mid = :"__scoping__#{type}__#{name}"
120
+ block = ->(target) { callable.call(self, target) } if callable
118
121
 
119
122
  define_method(mid, &block)
120
123
 
@@ -154,6 +157,14 @@ module ActionPolicy
154
157
  []
155
158
  end
156
159
  end
160
+
161
+ private
162
+
163
+ def prepare_args(name, callable)
164
+ return [name, callable] if name && callable
165
+ return [name, nil] if name.is_a?(Symbol) || name.is_a?(String)
166
+ [:default, name]
167
+ end
157
168
  end
158
169
  end
159
170
  end
@@ -48,7 +48,7 @@ module ActionPolicy
48
48
  def authorize!(record = :__undef__, to: nil, **options)
49
49
  to ||= :"#{action_name}?"
50
50
 
51
- super(record, to: to, **options)
51
+ super
52
52
 
53
53
  self.authorize_count += 1
54
54
  end
@@ -21,7 +21,7 @@ module ActionPolicy
21
21
  #
22
22
  class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
23
23
  attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
24
- :target_expectations
24
+ :target_expectations, :context
25
25
 
26
26
  def initialize(type)
27
27
  @type = type
@@ -49,6 +49,11 @@ module ActionPolicy
49
49
  self
50
50
  end
51
51
 
52
+ def with_context(context)
53
+ @context = context
54
+ self
55
+ end
56
+
52
57
  def match(_expected, actual)
53
58
  raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
54
59
 
@@ -56,7 +61,7 @@ module ActionPolicy
56
61
 
57
62
  @actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
58
63
 
59
- matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
64
+ matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
60
65
 
61
66
  return false if matching_scopes.empty?
62
67
 
@@ -80,6 +85,7 @@ module ActionPolicy
80
85
  def failure_message
81
86
  "expected a scoping named :#{name} for type :#{type} " \
82
87
  "#{scope_options_message} " \
88
+ "#{context ? "and context #{context.inspect} " : ""}" \
83
89
  "from #{policy} to have been applied, " \
84
90
  "but #{actual_scopes_message}"
85
91
  end
@@ -82,7 +82,7 @@ module ActionPolicy
82
82
  # end
83
83
  # end
84
84
  #
85
- def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil)
85
+ def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil, context: {})
86
86
  raise ArgumentError, "Block is required" unless block_given?
87
87
 
88
88
  policy = with
@@ -97,10 +97,13 @@ module ActionPolicy
97
97
  "without scope options"
98
98
  end
99
99
 
100
+ context_message = context.empty? ? "without context" : "with context: #{context}"
101
+
100
102
  assert(
101
- actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options) },
103
+ actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options, context) },
102
104
  "Expected a scoping named :#{as} for :#{type} type " \
103
105
  "#{scope_options_message} " \
106
+ "and #{context_message} " \
104
107
  "from #{policy} to have been applied, " \
105
108
  "but no such scoping has been made.\n" \
106
109
  "Registered scopings: " \
@@ -5,7 +5,19 @@ module ActionPolicy
5
5
  module Testing
6
6
  # Collects all Authorizer calls
7
7
  module AuthorizeTracker
8
+ module Context
9
+ private
10
+
11
+ def context_matches?(context, actual)
12
+ return true unless context
13
+
14
+ context === actual || actual >= context
15
+ end
16
+ end
17
+
8
18
  class Call # :nodoc:
19
+ include Context
20
+
9
21
  attr_reader :policy, :rule
10
22
 
11
23
  def initialize(policy, rule)
@@ -23,17 +35,11 @@ module ActionPolicy
23
35
  "#{policy.record.inspect} was authorized with #{policy.class}##{rule} " \
24
36
  "and context #{policy.authorization_context.inspect}"
25
37
  end
26
-
27
- private
28
-
29
- def context_matches?(context, actual)
30
- return true unless context
31
-
32
- context === actual || actual >= context
33
- end
34
38
  end
35
39
 
36
40
  class Scoping # :nodoc:
41
+ include Context
42
+
37
43
  attr_reader :policy, :target, :type, :name, :scope_options
38
44
 
39
45
  def initialize(policy, target, type, name, scope_options)
@@ -44,11 +50,12 @@ module ActionPolicy
44
50
  @scope_options = scope_options
45
51
  end
46
52
 
47
- def matches?(policy_class, actual_type, actual_name, actual_scope_options)
53
+ def matches?(policy_class, actual_type, actual_name, actual_scope_options, actual_context)
48
54
  policy_class == policy.class &&
49
55
  type == actual_type &&
50
56
  name == actual_name &&
51
- actual_scope_options === scope_options
57
+ actual_scope_options === scope_options &&
58
+ context_matches?(actual_context, policy.authorization_context)
52
59
  end
53
60
 
54
61
  def inspect
@@ -7,8 +7,7 @@ begin
7
7
  # Ignore parse warnings when patch
8
8
  # Ruby version mismatches
9
9
  $VERBOSE = nil
10
- require "parser/current"
11
- require "unparser"
10
+ require "prism"
12
11
  rescue LoadError
13
12
  # do nothing
14
13
  ensure
@@ -42,7 +41,7 @@ module ActionPolicy
42
41
  FALSE = "\e[31mfalse\e[0m"
43
42
 
44
43
  class Visitor
45
- attr_reader :lines, :object
44
+ attr_reader :lines, :object, :source
46
45
  attr_accessor :indent
47
46
 
48
47
  def initialize(object)
@@ -52,6 +51,8 @@ module ActionPolicy
52
51
  def collect(ast)
53
52
  @lines = []
54
53
  @indent = 0
54
+ @source = ast.source.source
55
+ ast = ast.value.child_nodes[0].child_nodes[0].body
55
56
 
56
57
  visit_node(ast)
57
58
 
@@ -67,7 +68,7 @@ module ActionPolicy
67
68
  end
68
69
 
69
70
  def expression_with_result(sexp)
70
- expression = Unparser.unparse(sexp)
71
+ expression = source[sexp.location.start_offset...sexp.location.end_offset]
71
72
  "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
73
  end
73
74
 
@@ -78,36 +79,33 @@ module ActionPolicy
78
79
  "Failed: #{e.message}"
79
80
  end
80
81
 
81
- def visit_and(ast)
82
- visit_node(ast.children[0])
82
+ def visit_and_node(ast)
83
+ visit_node(ast.left)
83
84
  lines << indented("AND")
84
- visit_node(ast.children[1])
85
+ visit_node(ast.right)
85
86
  end
86
87
 
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
88
+ def visit_or_node(ast)
89
+ visit_node(ast.left)
89
90
  lines << indented("OR")
90
- visit_node(ast.children[1])
91
+ visit_node(ast.right)
91
92
  end
92
93
 
93
- def visit_begin(ast)
94
- # Parens
95
- if ast.children.size == 1
96
- lines << indented("(")
97
- self.indent += 2
98
- visit_node(ast.children[0])
94
+ def visit_statements_node(ast)
95
+ ast.child_nodes.each do |node|
96
+ visit_node(node)
97
+ # restore indent after each expression
99
98
  self.indent -= 2
100
- lines << indented(")")
101
- else
102
- # Multiple expressions
103
- ast.children.each do |node|
104
- visit_node(node)
105
- # restore indent after each expression
106
- self.indent -= 2
107
- end
108
99
  end
109
100
  end
110
101
 
102
+ def visit_parentheses_node(ast)
103
+ lines << indented("(")
104
+ self.indent += 2
105
+ visit_node(ast.child_nodes[0])
106
+ lines << indented(")")
107
+ end
108
+
111
109
  def visit_missing(ast)
112
110
  lines << indented(expression_with_result(ast))
113
111
  end
@@ -128,15 +126,13 @@ module ActionPolicy
128
126
  class << self
129
127
  attr_accessor :ignore_expressions
130
128
 
131
- if defined?(::Unparser) && defined?(::MethodSource)
129
+ if defined?(::Prism) && defined?(::MethodSource)
132
130
  def available?() = true
133
131
 
134
132
  def print_method(object, method_name)
135
- ast = object.method(method_name).source.then(&Unparser.:parse)
136
- # outer node is a method definition itself
137
- body = ast.children[2]
133
+ ast = Prism.parse(object.method(method_name).source)
138
134
 
139
- Visitor.new(object).collect(body)
135
+ Visitor.new(object).collect(ast)
140
136
  end
141
137
  else
142
138
  def available?() = false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.6.8"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-17 00:00:00.000000000 Z
11
+ date: 2024-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -133,7 +133,6 @@ files:
133
133
  - LICENSE.txt
134
134
  - README.md
135
135
  - config/rubocop-rspec.yml
136
- - lib/.rbnext/1995.next/action_policy/utils/pretty_print.rb
137
136
  - lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb
138
137
  - lib/.rbnext/2.7/action_policy/i18n.rb
139
138
  - lib/.rbnext/2.7/action_policy/policy/cache.rb
@@ -143,6 +142,7 @@ files:
143
142
  - lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb
144
143
  - lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb
145
144
  - lib/.rbnext/2.7/action_policy/utils/pretty_print.rb
145
+ - lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb
146
146
  - lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb
147
147
  - lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb
148
148
  - lib/.rbnext/3.0/action_policy/policy/aliases.rb
@@ -234,7 +234,7 @@ metadata:
234
234
  documentation_uri: https://actionpolicy.evilmartians.io/
235
235
  homepage_uri: https://actionpolicy.evilmartians.io/
236
236
  source_code_uri: http://github.com/palkan/action_policy
237
- post_install_message:
237
+ post_install_message:
238
238
  rdoc_options: []
239
239
  require_paths:
240
240
  - lib
@@ -242,15 +242,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
242
242
  requirements:
243
243
  - - ">="
244
244
  - !ruby/object:Gem::Version
245
- version: 2.6.0
245
+ version: 2.7.0
246
246
  required_rubygems_version: !ruby/object:Gem::Requirement
247
247
  requirements:
248
248
  - - ">="
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
251
  requirements: []
252
- rubygems_version: 3.4.20
253
- signing_key:
252
+ rubygems_version: 3.4.19
253
+ signing_key:
254
254
  specification_version: 4
255
255
  summary: Authorization framework for Ruby/Rails application
256
256
  test_files: []
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- old_verbose = $VERBOSE
4
-
5
- begin
6
- require "method_source"
7
- # Ignore parse warnings when patch
8
- # Ruby version mismatches
9
- $VERBOSE = nil
10
- require "parser/current"
11
- require "unparser"
12
- rescue LoadError
13
- # do nothing
14
- ensure
15
- $VERBOSE = old_verbose
16
- end
17
-
18
- module ActionPolicy
19
- using RubyNext
20
-
21
- # Takes the object and a method name,
22
- # and returns the "annotated" source code for the method:
23
- # code is split into parts by logical operators and each
24
- # part is evaluated separately.
25
- #
26
- # Example:
27
- #
28
- # class MyClass
29
- # def access?
30
- # admin? && access_feed?
31
- # end
32
- # end
33
- #
34
- # puts PrettyPrint.format_method(MyClass.new, :access?)
35
- #
36
- # #=> MyClass#access?
37
- # #=> ↳ admin? #=> false
38
- # #=> AND
39
- # #=> access_feed? #=> true
40
- module PrettyPrint
41
- TRUE = "\e[32mtrue\e[0m"
42
- FALSE = "\e[31mfalse\e[0m"
43
-
44
- class Visitor
45
- attr_reader :lines, :object
46
- attr_accessor :indent
47
-
48
- def initialize(object)
49
- @object = object
50
- end
51
-
52
- def collect(ast)
53
- @lines = []
54
- @indent = 0
55
-
56
- visit_node(ast)
57
-
58
- lines.join("\n")
59
- end
60
-
61
- def visit_node(ast)
62
- if respond_to?("visit_#{ast.type}")
63
- send("visit_#{ast.type}", ast)
64
- else
65
- visit_missing ast
66
- end
67
- end
68
-
69
- def expression_with_result(sexp)
70
- expression = Unparser.unparse(sexp)
71
- "#{expression} #=> #{PrettyPrint.colorize(eval_exp(expression))}"
72
- end
73
-
74
- def eval_exp(exp)
75
- return "<skipped>" if ignore_exp?(exp)
76
- object.instance_eval(exp)
77
- rescue => e
78
- "Failed: #{e.message}"
79
- end
80
-
81
- def visit_and(ast)
82
- visit_node(ast.children[0])
83
- lines << indented("AND")
84
- visit_node(ast.children[1])
85
- end
86
-
87
- def visit_or(ast)
88
- visit_node(ast.children[0])
89
- lines << indented("OR")
90
- visit_node(ast.children[1])
91
- end
92
-
93
- def visit_begin(ast)
94
- # Parens
95
- if ast.children.size == 1
96
- lines << indented("(")
97
- self.indent += 2
98
- visit_node(ast.children[0])
99
- self.indent -= 2
100
- lines << indented(")")
101
- else
102
- # Multiple expressions
103
- ast.children.each do |node|
104
- visit_node(node)
105
- # restore indent after each expression
106
- self.indent -= 2
107
- end
108
- end
109
- end
110
-
111
- def visit_missing(ast)
112
- lines << indented(expression_with_result(ast))
113
- end
114
-
115
- def indented(str)
116
- "#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do
117
- # increase indent after the first expression
118
- self.indent += 2 if indent.zero?
119
- end
120
- end
121
-
122
- # Some lines should not be evaled
123
- def ignore_exp?(exp)
124
- PrettyPrint.ignore_expressions.any? { exp.match?(_1) }
125
- end
126
- end
127
-
128
- class << self
129
- attr_accessor :ignore_expressions
130
-
131
- if defined?(::Unparser) && defined?(::MethodSource)
132
- def available?() = true
133
-
134
- def print_method(object, method_name)
135
- ast = object.method(method_name).source.then(&Unparser.method(:parse))
136
- # outer node is a method definition itself
137
- body = ast.children[2]
138
-
139
- Visitor.new(object).collect(body)
140
- end
141
- else
142
- def available?() = false
143
-
144
- def print_method(_, _) = ""
145
- end
146
-
147
- def colorize(val)
148
- return val unless $stdout.isatty
149
- return TRUE if val.eql?(true) # rubocop:disable Lint/DeprecatedConstants
150
- return FALSE if val.eql?(false) # rubocop:disable Lint/DeprecatedConstants
151
- val
152
- end
153
- end
154
-
155
- self.ignore_expressions = [
156
- /^\s*binding\.(pry|irb)\s*$/s
157
- ]
158
- end
159
- end