action_policy 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd1a7f10e0934ff905e63da4fe68970165813f062ee10c2534eba30acbdcb04b
4
- data.tar.gz: 0e26b4d23cd11d96accad3c4ba15ff7dfbd68483b1592a98dff181afe6c699a3
3
+ metadata.gz: a261b126090b29222039294b84b4575fa796bf115033c2514fe301e0ec3d34af
4
+ data.tar.gz: 05f9d1cd84b2f4ce2b951267e0122fb54a436a5e51f0dd2f9a7d79e06f30d6ec
5
5
  SHA512:
6
- metadata.gz: b9643d2b9298058b34d42717b25cb258f53d8cd1aa4314889fab4c830fe781af82e3688400453c1edb0fd41273c5abcdf77f36031d6afc984ececb666570b4f0
7
- data.tar.gz: ea54f95f99eeb8e60f0554c9bc4b1ae1dccd235c34efba012c08e7a222f3ea1b6fcb873bc19fad1f147af526724e326ad913e6663a8e88b072b315f0e25e3ac4
6
+ metadata.gz: 80bbbe6ec61bf7241fe0e823844120292e6b8efd98a3c06834d98ac797cf92ebdffa21332e40305770622d5120d391443d0570193001a5e358f1ee6056c09bbb
7
+ data.tar.gz: 7f6990f7bcd5998eef536640a451591801afd073e29b25a764c500b1d0f535e3cfaf1a176f0adf930f498386ca9f206aa377b4c5adf1b2d119be642681fa7e81
data/.rubocop.yml CHANGED
@@ -39,6 +39,10 @@ Naming/FileName:
39
39
  - 'Gemfile'
40
40
  - '**/*.md'
41
41
 
42
+ Layout/InitialIndentation:
43
+ Exclude:
44
+ - 'CHANGELOG.md'
45
+
42
46
  Naming/UncommunicativeMethodParamName:
43
47
  Enabled: false
44
48
 
data/.travis.yml CHANGED
@@ -11,7 +11,7 @@ matrix:
11
11
  include:
12
12
  - rvm: ruby-head
13
13
  gemfile: gemfiles/railsmaster.gemfile
14
- - rvm: jruby-9.1.0.0
14
+ - rvm: jruby-9.2.0.0
15
15
  gemfile: gemfiles/jruby.gemfile
16
16
  - rvm: 2.5.1
17
17
  gemfile: Gemfile
@@ -24,5 +24,5 @@ matrix:
24
24
  allow_failures:
25
25
  - rvm: ruby-head
26
26
  gemfile: gemfiles/railsmaster.gemfile
27
- - rvm: jruby-9.1.0.0
27
+ - rvm: jruby-9.2.0.0
28
28
  gemfile: gemfiles/jruby.gemfile
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## master
2
2
 
3
+ ## 0.2.0 (2018-06-17)
4
+
5
+ - Make `action_policy` JRuby-compatible. ([@palkan][])
6
+
7
+ - Add `reasons.details`. ([@palkan][])
8
+
9
+ ```ruby
10
+ rescue_from ActionPolicy::Unauthorized do |ex|
11
+ ex.result.reasons.details #=> { stage: [:show?] }
12
+ end
13
+ ```
14
+
15
+ - Add `ExecutionResult`. ([@palkan][])
16
+
17
+ ExecutionResult contains all the rule application artifacts: the result (`true` / `false`),
18
+ failures reasons.
19
+
20
+ This value is now stored in a cache (if any) instead of just the call result (`true` / `false`).
21
+
22
+ - Add `Policy.identifier`. ([@palkan][])
23
+
3
24
  ## 0.1.4 (2018-06-06)
4
25
 
5
26
  - Fix Railtie injection hook. ([@palkan][])
data/docs/caching.md CHANGED
@@ -155,7 +155,9 @@ Rails.application.configure do |config|
155
155
  end
156
156
  ```
157
157
 
158
- Cache store must provide at least a `#fetch(key, **options, &block)` method.
158
+ Cache store must provide at least a `#read(key)` and `write(key, value, **options)` methods.
159
+
160
+ **NOTE:** cache store also should take care of serialiation/deserialization since the `value` is `ExecutionResult` instance (which contains also some additional information, e.g. failure reasons). Rails cache store supports serialization/deserialization out-of-the-box.
159
161
 
160
162
  By default, Action Policy builds a cache key using the following scheme:
161
163
 
@@ -10,10 +10,12 @@ It looks like this:
10
10
  class ActionPolicy::Base
11
11
  include ActionPolicy::Policy::Core
12
12
  include ActionPolicy::Policy::Authorization
13
- include ActionPolicy::Policy::Reasons
14
13
  include ActionPolicy::Policy::PreCheck
14
+ include ActionPolicy::Policy::Reasons
15
15
  include ActionPolicy::Policy::Aliases
16
+ include ActionPolicy::Policy::Cache
16
17
  include ActionPolicy::Policy::CachedApply
18
+ include ActionPolicy::Policy::Defaults
17
19
 
18
20
  # ActionPolicy::Policy::Defaults module adds the following
19
21
 
data/docs/quick_start.md CHANGED
@@ -74,7 +74,15 @@ end
74
74
 
75
75
  In the above case, Action Policy automatically infers a policy class and a rule to verify access: `@post -> Post -> PostPolicy`, rule is inferred from the action name (`update -> update?`), and `current_user` is used as `user` within the policy by default (read more about [authorization context](authorization_context.md)).
76
76
 
77
- When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised.
77
+ When authorization is successful (i.e., the corresponding rule returns `true`), nothing happens, but in case of an authorization failure `ActionPolicy::Unauthorized` error is raised:
78
+
79
+ ```ruby
80
+ rescue_from ActionPolicy::Unauthorized do |ex|
81
+ # Exception object contains the following information
82
+ ex.policy #=> policy class, e.g. UserPolicy
83
+ ex.rule #=> applied rule, e.g. :show?
84
+ end
85
+ ```
78
86
 
79
87
  There is also an `allowed_to?` method which returns `true` or `false` and could be used, for example, in views:
80
88
 
data/docs/reasons.md CHANGED
@@ -17,16 +17,23 @@ class ApplicantPolicy < ApplicationPolicy
17
17
  end
18
18
  ```
19
19
 
20
- When `ApplicantPolicy#show?` check fails, the exception has the `reasons` object, which contains additional information about the failure:
20
+ When `ApplicantPolicy#show?` check fails, the exception has the `result` object, which in its turn contains additional information about the failure (`reasons`):
21
21
 
22
22
  ```ruby
23
23
  class ApplicationController < ActionController::Base
24
24
  rescue_from ActionPolicy::Unauthorized do |ex|
25
- p ex.reasons.messages #=> { stage: [:show?] }
25
+ p ex.result.reasons.details #=> { stage: [:show?] }
26
+
27
+ # or with i18n support
28
+ p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
26
29
  end
27
30
  end
28
31
  ```
29
32
 
33
+ The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
34
+
35
+ **NOTE:** `full_messages` support hasn't been released yet. See [the issue](https://github.com/palkan/action_policy/issues/15).
36
+
30
37
  You can also wrap _local_ rules into `allowed_to?` to populate reasons:
31
38
 
32
39
  ```ruby
@@ -42,12 +49,14 @@ class ApplicantPolicy < ApplicationPolicy
42
49
  end
43
50
 
44
51
  # then the reasons object could be
45
- p ex.reasons.messages #=> { applicant: [:view_applicants?] }
52
+ p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
46
53
 
47
54
  # or
48
- p ex.reasons.messages #=> { stage: [:show?] }
55
+ p ex.result.reasons.details #=> { stage: [:show?] }
49
56
  ```
50
57
 
58
+
59
+
51
60
  **What is the point of failure reasons?**
52
61
 
53
62
  First, you can provide a user with helpful feedback. For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
@@ -53,3 +53,39 @@ end
53
53
  ```
54
54
 
55
55
  You can also specify all the usual options (such as `with`).
56
+
57
+ ## Identifiers
58
+
59
+ Each policy class has an `identifier`, which is by default just an underscored class name:
60
+
61
+ ```ruby
62
+ class CommentPolicy < ApplicationPolicy
63
+ end
64
+
65
+ CommentPolicy.identifier #=> :comment
66
+ ```
67
+
68
+ For namespaced policies it has a form of:
69
+
70
+ ```ruby
71
+ module ActiveAdmin
72
+ class UserPolicy < ApplicationPolicy
73
+ end
74
+ end
75
+
76
+ ActiveAdmin::UserPolicy.identifier # => :"active_admin/user"
77
+ ```
78
+
79
+ You can specify your own identifier:
80
+
81
+ ```ruby
82
+ module MyVeryLong
83
+ class LongLongNamePolicy < ApplicationPolicy
84
+ self.identifier = :long_name
85
+ end
86
+ end
87
+
88
+ MyVeryLong::LongLongNamePolicy.identifier #=> :long_name
89
+ ```
90
+
91
+ Identifiers are required for some modules, such as [failure reasons tracking](reasons.md) and [i18n](i18n.md).
@@ -3,13 +3,12 @@
3
3
  module ActionPolicy
4
4
  # Raised when `authorize!` check fails
5
5
  class Unauthorized < Error
6
- attr_reader :policy, :rule, :reasons
6
+ attr_reader :policy, :rule, :result
7
7
 
8
8
  def initialize(policy, rule)
9
9
  @policy = policy.class
10
10
  @rule = rule
11
- # Reasons module could be not included
12
- @reasons = policy.reasons if policy.respond_to?(:reasons)
11
+ @result = policy.result
13
12
  end
14
13
  end
15
14
 
@@ -3,24 +3,31 @@
3
3
  module ActionPolicy
4
4
  module Ext
5
5
  # Add Module#namespace method
6
- module ModuleNamespace
7
- refine Module do
8
- unless "".respond_to?(:safe_constantize)
9
- require "action_policy/ext/string_constantize"
10
- using ActionPolicy::Ext::StringConstantize
11
- end
6
+ module ModuleNamespace # :nodoc: all
7
+ unless "".respond_to?(:safe_constantize)
8
+ require "action_policy/ext/string_constantize"
9
+ using ActionPolicy::Ext::StringConstantize
10
+ end
12
11
 
13
- unless "".respond_to?(:match?)
14
- require "action_policy/ext/string_match"
15
- using ActionPolicy::Ext::StringMatch
16
- end
12
+ unless "".respond_to?(:match?)
13
+ require "action_policy/ext/string_match"
14
+ using ActionPolicy::Ext::StringMatch
15
+ end
17
16
 
17
+ module Ext
18
18
  def namespace
19
19
  return unless name.match?(/[^^]::/)
20
20
 
21
21
  name.sub(/::[^:]+$/, "").safe_constantize
22
22
  end
23
23
  end
24
+
25
+ # See https://github.com/jruby/jruby/issues/5220
26
+ ::Module.include(Ext) if RUBY_PLATFORM =~ /java/i
27
+
28
+ refine Module do
29
+ include Ext
30
+ end
24
31
  end
25
32
  end
26
33
  end
@@ -9,8 +9,8 @@ module ActionPolicy
9
9
  # For other core classes returns string representation.
10
10
  #
11
11
  # Raises ArgumentError otherwise.
12
- module PolicyCacheKey
13
- refine Object do
12
+ module PolicyCacheKey # :nodoc: all
13
+ module ObjectExt
14
14
  def _policy_cache_key(use_object_id: false)
15
15
  return policy_cache_key if respond_to?(:policy_cache_key)
16
16
  return cache_key if respond_to?(:cache_key)
@@ -21,6 +21,14 @@ module ActionPolicy
21
21
  end
22
22
  end
23
23
 
24
+ # JRuby doesn't support _global_ modules refinements (see https://github.com/jruby/jruby/issues/5220)
25
+ # Fallback to monkey-patching.
26
+ ::Object.include(ObjectExt) if RUBY_PLATFORM =~ /java/i
27
+
28
+ refine Object do
29
+ include ObjectExt
30
+ end
31
+
24
32
  refine NilClass do
25
33
  def _policy_cache_key(*)
26
34
  ""
@@ -51,9 +59,23 @@ module ActionPolicy
51
59
  end
52
60
  end
53
61
 
54
- refine Numeric do
55
- def _policy_cache_key(*)
56
- to_s
62
+ if RUBY_PLATFORM =~ /java/i
63
+ refine Integer do
64
+ def _policy_cache_key(*)
65
+ to_s
66
+ end
67
+ end
68
+
69
+ refine Float do
70
+ def _policy_cache_key(*)
71
+ to_s
72
+ end
73
+ end
74
+ else
75
+ refine Numeric do
76
+ def _policy_cache_key(*)
77
+ to_s
78
+ end
57
79
  end
58
80
  end
59
81
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext
5
+ # Add underscore to String
6
+ module StringUnderscore
7
+ refine String do
8
+ def underscore
9
+ word = gsub(/::/, "/")
10
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
11
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
12
+ word.downcase!
13
+ word
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Ext # :nodoc: all
5
+ # Add yield_self and then if missing
6
+ module YieldSelfThen
7
+ module Ext
8
+ unless nil.respond_to?(:yield_self)
9
+ def yield_self
10
+ yield self
11
+ end
12
+ end
13
+
14
+ alias then yield_self
15
+ end
16
+
17
+ # See https://github.com/jruby/jruby/issues/5220
18
+ ::Object.include(Ext) if RUBY_PLATFORM =~ /java/i
19
+
20
+ refine Object do
21
+ include Ext
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,7 +7,7 @@ module ActionPolicy
7
7
  #
8
8
  # class ApplicationPolicy
9
9
  # include ActionPolicy::Policy::Core
10
- # prepend ActionPolicy::Policy::Aliases
10
+ # include ActionPolicy::Policy::Aliases
11
11
  #
12
12
  # # define which rule to use if `authorize!` called with
13
13
  # # unknown rule
@@ -23,19 +23,14 @@ module ActionPolicy
23
23
  DEFAULT = :__default__
24
24
 
25
25
  class << self
26
- def prepended(base)
26
+ def included(base)
27
27
  base.extend ClassMethods
28
- base.prepend InstanceMethods
29
28
  end
30
-
31
- alias included prepended
32
29
  end
33
30
 
34
- module InstanceMethods # :nodoc:
35
- def resolve_rule(activity)
36
- return activity if respond_to?(activity)
37
- self.class.lookup_alias(activity) || super
38
- end
31
+ def resolve_rule(activity)
32
+ return activity if respond_to?(activity)
33
+ self.class.lookup_alias(activity) || super
39
34
  end
40
35
 
41
36
  module ClassMethods # :nodoc:
@@ -36,34 +36,29 @@ module ActionPolicy
36
36
  # ApplicantPolicy.new(user: user, account: account)
37
37
  module Authorization
38
38
  class << self
39
- def prepended(base)
39
+ def included(base)
40
40
  base.extend ClassMethods
41
- base.prepend InstanceMethods
42
41
  end
43
-
44
- alias included prepended
45
42
  end
46
43
 
47
44
  attr_reader :authorization_context
48
45
 
49
- module InstanceMethods # :nodoc:
50
- def initialize(*args, **params)
51
- super(*args)
52
-
53
- @authorization_context = {}
46
+ def initialize(*args, **params)
47
+ super(*args)
54
48
 
55
- self.class.authorization_targets.each do |id, opts|
56
- raise AuthorizationContextMissing, id unless params.key?(id)
49
+ @authorization_context = {}
57
50
 
58
- val = params.fetch(id)
51
+ self.class.authorization_targets.each do |id, opts|
52
+ raise AuthorizationContextMissing, id unless params.key?(id)
59
53
 
60
- raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
54
+ val = params.fetch(id)
61
55
 
62
- authorization_context[id] = instance_variable_set("@#{id}", val)
63
- end
56
+ raise AuthorizationContextMissing, id if val.nil? && opts[:allow_nil] != true
64
57
 
65
- authorization_context.freeze
58
+ authorization_context[id] = instance_variable_set("@#{id}", val)
66
59
  end
60
+
61
+ authorization_context.freeze
67
62
  end
68
63
 
69
64
  module ClassMethods # :nodoc:
@@ -6,7 +6,10 @@ module ActionPolicy # :nodoc:
6
6
  # By default cache namespace (or prefix) contains major and minor version of the gem
7
7
  CACHE_NAMESPACE = "acp:#{ActionPolicy::VERSION.split('.').take(2).join('.')}"
8
8
 
9
+ require "action_policy/ext/yield_self_then"
9
10
  require "action_policy/ext/policy_cache_key"
11
+
12
+ using ActionPolicy::Ext::YieldSelfThen
10
13
  using ActionPolicy::Ext::PolicyCacheKey
11
14
 
12
15
  module Policy
@@ -15,12 +18,9 @@ module ActionPolicy # :nodoc:
15
18
  # NOTE: if cache_store is nil then we silently skip all the caching.
16
19
  module Cache
17
20
  class << self
18
- def prepended(base)
21
+ def included(base)
19
22
  base.extend ClassMethods
20
- base.prepend InstanceMethods
21
23
  end
22
-
23
- alias included prepended
24
24
  end
25
25
 
26
26
  def cache_namespace
@@ -36,19 +36,26 @@ module ActionPolicy # :nodoc:
36
36
  authorization_context.map { |_k, v| v._policy_cache_key.to_s }.join("/")
37
37
  end
38
38
 
39
+ # rubocop: disable Metrics/AbcSize
39
40
  def apply_with_cache(rule)
40
41
  options = self.class.cached_rules.fetch(rule)
42
+ key = cache_key(rule)
41
43
 
42
- ActionPolicy.cache_store.fetch(cache_key(rule), options) { yield }
44
+ ActionPolicy.cache_store.then do |store|
45
+ @result = store.read(key)
46
+ next result.value unless result.nil?
47
+ yield
48
+ store.write(key, result, options)
49
+ result.value
50
+ end
43
51
  end
52
+ # rubocop: enable Metrics/AbcSize
44
53
 
45
- module InstanceMethods # :nodoc:
46
- def apply(rule)
47
- return super if ActionPolicy.cache_store.nil? ||
48
- !self.class.cached_rules.key?(rule)
54
+ def apply(rule)
55
+ return super if ActionPolicy.cache_store.nil? ||
56
+ !self.class.cached_rules.key?(rule)
49
57
 
50
- apply_with_cache(rule) { super }
51
- end
58
+ apply_with_cache(rule) { super }
52
59
  end
53
60
 
54
61
  module ClassMethods # :nodoc:
@@ -7,21 +7,19 @@ module ActionPolicy
7
7
  # When you call `apply` twice on the same policy and for the same rule,
8
8
  # the check (and pre-checks) is only called once.
9
9
  module CachedApply
10
- class << self
11
- def prepended(base)
12
- base.prepend InstanceMethods
10
+ def apply(rule)
11
+ @__rules_cache__ ||= {}
12
+
13
+ if @__rules_cache__.key?(rule)
14
+ @result = @__rules_cache__[rule]
15
+ return result.value
13
16
  end
14
17
 
15
- alias included prepended
16
- end
18
+ super
17
19
 
18
- module InstanceMethods # :nodoc:
19
- def apply(rule)
20
- @__rules_cache__ ||= {}
21
- return @__rules_cache__[rule] if @__rules_cache__.key?(rule)
20
+ @__rules_cache__[rule] = result
22
21
 
23
- @__rules_cache__[rule] = super
24
- end
22
+ result.value
25
23
  end
26
24
  end
27
25
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_policy/behaviours/policy_for"
4
+ require "action_policy/policy/execution_result"
5
+
6
+ unless "".respond_to?(:underscore)
7
+ require "action_policy/ext/string_underscore"
8
+ using ActionPolicy::Ext::StringUnderscore
9
+ end
4
10
 
5
11
  module ActionPolicy
6
12
  # Raised when `resolve_rule` failed to find an approriate
@@ -18,35 +24,80 @@ module ActionPolicy
18
24
  module Policy
19
25
  # Core policy API
20
26
  module Core
27
+ class << self
28
+ def included(base)
29
+ base.extend ClassMethods
30
+
31
+ # Generate a new class for each _policy chain_
32
+ # in order to extend it independently
33
+ base.module_eval do
34
+ @result_class = Class.new(ExecutionResult)
35
+
36
+ # we need to make this class _named_,
37
+ # 'cause anonymous classes couldn't be marshalled
38
+ base.const_set(:APR, @result_class)
39
+ end
40
+ end
41
+ end
42
+
43
+ module ClassMethods # :nodoc:
44
+ attr_writer :identifier
45
+
46
+ def result_class
47
+ return @result_class if instance_variable_defined?(:@result_class)
48
+ @result_class = superclass.result_class
49
+ end
50
+
51
+ def identifier
52
+ return @identifier if instance_variable_defined?(:@identifier)
53
+
54
+ @identifier = name.sub(/Policy$/, "").underscore.to_sym
55
+ end
56
+ end
57
+
21
58
  include ActionPolicy::Behaviours::PolicyFor
22
59
 
23
- attr_reader :record
60
+ attr_reader :record, :result
24
61
 
25
62
  def initialize(record = nil)
26
63
  @record = record
27
64
  end
28
65
 
29
- # Returns a result of applying the specified rule.
66
+ # Returns a result of applying the specified rule (true of false).
30
67
  # Unlike simply calling a predicate rule (`policy.manage?`),
31
68
  # `apply` also calls pre-checks.
32
69
  def apply(rule)
70
+ @result = self.class.result_class.new
71
+ @result.load __apply__(rule)
72
+ end
73
+
74
+ # This method performs the rule call.
75
+ # Override or extend it to provide custom functionality
76
+ # (such as caching, pre checks, etc.)
77
+ def __apply__(rule)
33
78
  public_send(rule)
34
79
  end
35
80
 
81
+ # Wrap code that could modify result
82
+ # to prevent the current result modification
83
+ def with_clean_result
84
+ was_result = @result
85
+ res = yield
86
+ @result = was_result
87
+ res
88
+ end
89
+
36
90
  # Returns a result of applying the specified rule to the specified record.
37
91
  # Under the hood a policy class for record is resolved
38
92
  # (unless it's explicitly set through `with` option).
39
93
  #
40
94
  # If record is `nil` then we uses the current policy.
41
95
  def allowed_to?(rule, record = :__undef__, **options)
42
- policy =
43
- if record == :__undef__
44
- self
45
- else
46
- policy_for(record: record, **options)
47
- end
48
-
49
- policy.apply(rule)
96
+ if record == :__undef__
97
+ __apply__(rule)
98
+ else
99
+ policy_for(record: record, **options).apply(rule)
100
+ end
50
101
  end
51
102
 
52
103
  # Returns a rule name (policy method name) for activity.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Policy
5
+ # Result of applying a policy rule
6
+ #
7
+ # This class could be extended by some modules to provide
8
+ # additional functionality
9
+ class ExecutionResult
10
+ attr_reader :value
11
+
12
+ # Populate the final value
13
+ def load(value)
14
+ @value = value
15
+ end
16
+
17
+ def success?
18
+ @value == true
19
+ end
20
+
21
+ def fail?
22
+ @value == false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -83,8 +83,8 @@ module ActionPolicy
83
83
  def call(policy)
84
84
  Result.new(policy.send(name)).tap do |res|
85
85
  # add denial reason if Reasons included
86
- policy.reasons.add(policy_class, name) if
87
- res.denied? && policy.respond_to?(:reasons)
86
+ policy.result.reasons.add(policy, name) if
87
+ res.denied? && policy.result.respond_to?(:reasons)
88
88
  end
89
89
  end
90
90
 
@@ -136,12 +136,9 @@ module ActionPolicy
136
136
  end
137
137
 
138
138
  class << self
139
- def prepended(base)
139
+ def included(base)
140
140
  base.extend ClassMethods
141
- base.prepend InstanceMethods
142
141
  end
143
-
144
- alias included prepended
145
142
  end
146
143
 
147
144
  def run_pre_checks(rule)
@@ -162,10 +159,8 @@ module ActionPolicy
162
159
  Check::Result::ALLOW
163
160
  end
164
161
 
165
- module InstanceMethods # :nodoc:
166
- def apply(rule)
167
- run_pre_checks(rule) { super }
168
- end
162
+ def __apply__(rule)
163
+ run_pre_checks(rule) { super }
169
164
  end
170
165
 
171
166
  module ClassMethods # :nodoc:
@@ -2,28 +2,33 @@
2
2
 
3
3
  module ActionPolicy
4
4
  module Policy
5
- class FailureReason # :nodoc:
6
- attr_reader :policy, :rule
5
+ # Failures reasons store
6
+ class FailureReasons
7
+ attr_reader :details
7
8
 
8
- def initialize(policy_or_class, rule)
9
- @policy = policy_or_class.is_a?(Class) ? policy_or_class : policy_or_class.class
10
- @rule = rule
9
+ def initialize
10
+ @details = {}
11
11
  end
12
- end
13
12
 
14
- # Failures reasons store
15
- class FailureReasons
16
- include Enumerable
17
- extend Forwardable
13
+ def add(policy_or_class, rule)
14
+ policy_class = policy_or_class.is_a?(Class) ? policy_or_class : policy_or_class.class
15
+ details[policy_class.identifier] ||= []
16
+ details[policy_class.identifier] << rule
17
+ end
18
18
 
19
- def_delegators :@reasons, :size, :empty?, :last, :each
19
+ def empty?
20
+ details.empty?
21
+ end
20
22
 
21
- def initialize
22
- @reasons = []
23
+ def present?
24
+ !empty?
23
25
  end
26
+ end
24
27
 
25
- def add(policy, rule)
26
- @reasons << FailureReason.new(policy, rule)
28
+ # Extend ExecutionResult with `reasons` method
29
+ module ResultFailureReasons
30
+ def reasons
31
+ @reasons ||= FailureReasons.new
27
32
  end
28
33
  end
29
34
 
@@ -45,9 +50,21 @@ module ActionPolicy
45
50
  # information about the failure:
46
51
  #
47
52
  # rescue_from ActionPolicy::Unauthorized do |ex|
48
- # ex.reasons.messages #=> { stage: [:show] }
53
+ # ex.policy #=> ApplicantPolicy
54
+ # ex.rule #=> :show?
55
+ # ex.result.reasons.details #=> { stage: [:show?] }
56
+ # end
57
+ #
58
+ # NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default).
59
+ # For namespaced policies it has a form of:
60
+ #
61
+ # class Admin::UserPolicy < ApplicationPolicy
62
+ # # ..
49
63
  # end
50
64
  #
65
+ # reasons.details #=> { :"admin/user" => [:show?] }
66
+ #
67
+ #
51
68
  # You can also wrap _local_ rules into `allowed_to?` to populate reasons:
52
69
  #
53
70
  # class ApplicantPolicy < ApplicationPolicy
@@ -62,48 +79,29 @@ module ActionPolicy
62
79
  # end
63
80
  module Reasons
64
81
  class << self
65
- def prepended(base)
66
- base.prepend InstanceMethods
82
+ def included(base)
83
+ base.result_class.include(ResultFailureReasons)
67
84
  end
68
-
69
- alias included prepended
70
85
  end
71
86
 
72
- attr_reader :reasons
87
+ # rubocop: disable Metrics/MethodLength
88
+ def allowed_to?(rule, record = :__undef__, **options)
89
+ policy = nil
73
90
 
74
- def with_clean_reasons # :nodoc:
75
- old_reasons = reasons
76
- @reasons = nil
77
- res = yield
78
- @reasons = old_reasons
79
- res
80
- end
91
+ succeed =
92
+ if record == :__undef__
93
+ policy = self
94
+ with_clean_result { apply(rule) }
95
+ else
96
+ policy = policy_for(record: record, **options)
81
97
 
82
- module InstanceMethods # :nodoc:
83
- def apply(rule)
84
- @reasons = FailureReasons.new
85
- super
86
- end
87
-
88
- # rubocop: disable Metrics/MethodLength
89
- def allowed_to?(rule, record = :__undef__, **options)
90
- policy = nil
91
-
92
- succeed =
93
- if record == :__undef__
94
- policy = self
95
- with_clean_reasons { apply(rule) }
96
- else
97
- policy = policy_for(record: record, **options)
98
+ policy.apply(rule)
99
+ end
98
100
 
99
- policy.apply(rule)
100
- end
101
-
102
- reasons.add(policy, rule) if reasons && !succeed
103
- succeed
104
- end
105
- # rubocop: enable Metrics/MethodLength
101
+ result.reasons.add(policy, rule) unless succeed
102
+ succeed
106
103
  end
104
+ # rubocop: enable Metrics/MethodLength
107
105
  end
108
106
  end
109
107
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionPolicy
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.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.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-06 00:00:00.000000000 Z
11
+ date: 2018-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -163,6 +163,8 @@ files:
163
163
  - lib/action_policy/ext/policy_cache_key.rb
164
164
  - lib/action_policy/ext/string_constantize.rb
165
165
  - lib/action_policy/ext/string_match.rb
166
+ - lib/action_policy/ext/string_underscore.rb
167
+ - lib/action_policy/ext/yield_self_then.rb
166
168
  - lib/action_policy/lookup_chain.rb
167
169
  - lib/action_policy/policy/aliases.rb
168
170
  - lib/action_policy/policy/authorization.rb
@@ -170,6 +172,7 @@ files:
170
172
  - lib/action_policy/policy/cached_apply.rb
171
173
  - lib/action_policy/policy/core.rb
172
174
  - lib/action_policy/policy/defaults.rb
175
+ - lib/action_policy/policy/execution_result.rb
173
176
  - lib/action_policy/policy/pre_check.rb
174
177
  - lib/action_policy/policy/reasons.rb
175
178
  - lib/action_policy/rails/channel.rb