pundit-matchers 2.3.0 → 3.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pundit/matchers/actions_matcher.rb +41 -0
  3. data/lib/pundit/matchers/attributes_matcher.rb +71 -0
  4. data/lib/pundit/matchers/base_matcher.rb +27 -0
  5. data/lib/pundit/matchers/forbid_all_actions_matcher.rb +43 -0
  6. data/lib/pundit/matchers/forbid_only_actions_matcher.rb +60 -0
  7. data/lib/pundit/matchers/permit_actions_matcher.rb +69 -0
  8. data/lib/pundit/matchers/permit_all_actions_matcher.rb +43 -0
  9. data/lib/pundit/matchers/permit_attributes_matcher.rb +65 -0
  10. data/lib/pundit/matchers/permit_only_actions_matcher.rb +60 -0
  11. data/lib/pundit/matchers/utils/policy_info.rb +67 -6
  12. data/lib/pundit/matchers.rb +120 -364
  13. metadata +16 -39
  14. data/lib/pundit/matchers/utils/all_actions/actions_matcher.rb +0 -39
  15. data/lib/pundit/matchers/utils/all_actions/error_message_formatter.rb +0 -47
  16. data/lib/pundit/matchers/utils/all_actions/forbidden_actions_error_formatter.rb +0 -26
  17. data/lib/pundit/matchers/utils/all_actions/forbidden_actions_matcher.rb +0 -20
  18. data/lib/pundit/matchers/utils/all_actions/permitted_actions_error_formatter.rb +0 -26
  19. data/lib/pundit/matchers/utils/all_actions/permitted_actions_matcher.rb +0 -20
  20. data/lib/pundit/matchers/utils/only_actions/actions_matcher.rb +0 -39
  21. data/lib/pundit/matchers/utils/only_actions/error_message_formatter.rb +0 -54
  22. data/lib/pundit/matchers/utils/only_actions/forbidden_actions_error_formatter.rb +0 -26
  23. data/lib/pundit/matchers/utils/only_actions/forbidden_actions_matcher.rb +0 -20
  24. data/lib/pundit/matchers/utils/only_actions/permitted_actions_error_formatter.rb +0 -26
  25. data/lib/pundit/matchers/utils/only_actions/permitted_actions_matcher.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eba90ec029bb4c7c2559d3660f4c63a20a5f6a08296e9aea787bb88ee9eb81c
4
- data.tar.gz: 9bb49382052dff3484503d94d61db694ded688f451db5c238ee93c665081661b
3
+ metadata.gz: 96b6768ee2dad9c91d75c12506f76d530b9b6052fc8d8b09c6fdaa56f88d1748
4
+ data.tar.gz: a2e24ef15671dec4fd9820ef5f61029499d16fd56a726be2dcdd5892dc02088f
5
5
  SHA512:
6
- metadata.gz: e449b597f5dcffd05af7b642f63a5fd36960784b4eabb07d92ecd165c6fe9b6809bb2c6f540b93d3fff9dbe46b7678f64aeb276064e5a4da3084bce7db0bfdd6
7
- data.tar.gz: ee59374a8518b01d267807ebbc78a345092ceec8f0f78f7b11deaa4753890ba78df4dcc579bd5658938efc685bde64e39ebec0a0be6fec3e813ea04d4c7b7969
6
+ metadata.gz: 0aa08b14749957cdeb61631b97712288eb9207201197e35f303892ab90730c5dd1fdd736f01f9d84e1c6665131d79d5a5bdc1028e2f6d4b34e875e5850e71871
7
+ data.tar.gz: 4fab87c430012868c5274767a5bc4d75da673e3c7637ea544d83eb244604548c17be7a21cea94617b940b42bfabc9b2f4c8fc6ab6682e18d55d2a527f5afba61
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This is the base action matcher class. Matchers related to actions should inherit from this class.
8
+ class ActionsMatcher < BaseMatcher
9
+ # Error message when actions are not implemented in a policy.
10
+ ACTIONS_NOT_IMPLEMENTED_ERROR = "'%<policy>s' does not implement %<actions>s"
11
+ # Error message when at least one action must be specified.
12
+ ARGUMENTS_REQUIRED_ERROR = 'At least one action must be specified'
13
+
14
+ # Initializes a new instance of the ActionsMatcher class.
15
+ #
16
+ # @param expected_actions [Array<String, Symbol>] The expected actions to be checked.
17
+ #
18
+ # @raise [ArgumentError] If no actions are specified.
19
+ def initialize(*expected_actions)
20
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_actions.empty?
21
+
22
+ super()
23
+ @expected_actions = expected_actions.flatten.map(&:to_sym).sort
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :expected_actions
29
+
30
+ def check_actions!
31
+ missing_actions = expected_actions - policy_info.actions
32
+ return if missing_actions.empty?
33
+
34
+ raise ArgumentError, format(
35
+ ACTIONS_NOT_IMPLEMENTED_ERROR,
36
+ policy: policy_info, actions: missing_actions
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # The AttributesMatcher class is used to test whether a Pundit policy allows or denies access to certain attributes.
8
+ class AttributesMatcher < BaseMatcher
9
+ # Error message to be raised when no attributes are specified.
10
+ ARGUMENTS_REQUIRED_ERROR = 'At least one attribute must be specified'
11
+
12
+ # Initializes a new instance of the AttributesMatcher class.
13
+ #
14
+ # @param expected_attributes [Array<String, Symbol, Hash>] The list of attributes to be tested.
15
+ def initialize(*expected_attributes)
16
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_attributes.empty?
17
+
18
+ super()
19
+ @expected_attributes = flatten_attributes(expected_attributes)
20
+ @options = {}
21
+ end
22
+
23
+ # Specifies the action to be tested.
24
+ #
25
+ # @param action [Symbol, String] The action to be tested.
26
+ # @return [AttributesMatcher] The current instance of the AttributesMatcher class.
27
+ def for_action(action)
28
+ @options[:action] = action
29
+ self
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :expected_attributes, :options
35
+
36
+ def permitted_attributes(policy)
37
+ @permitted_attributes ||=
38
+ if options.key?(:action)
39
+ flatten_attributes(policy.public_send(:"permitted_attributes_for_#{options[:action]}"))
40
+ else
41
+ flatten_attributes(policy.permitted_attributes)
42
+ end
43
+ end
44
+
45
+ def action_message
46
+ " when authorising the '#{options[:action]}' action"
47
+ end
48
+
49
+ # Flattens and sorts a hash or array of attributes into an array of symbols.
50
+ #
51
+ # This is a private method used internally by the `Matcher` class to convert
52
+ # attribute lists into a flattened, sorted array of symbols. The resulting
53
+ # array can be used to compare attribute lists.
54
+ #
55
+ # @param attributes [String, Symbol, Array, Hash] the attributes to be flattened.
56
+ # @return [Array<Symbol>] the flattened, sorted array of symbols.
57
+ def flatten_attributes(attributes)
58
+ case attributes
59
+ when String, Symbol
60
+ [attributes.to_sym]
61
+ when Array
62
+ attributes.flat_map { |item| flatten_attributes(item) }.sort
63
+ when Hash
64
+ attributes.flat_map do |key, value|
65
+ flatten_attributes(value).map { |item| :"#{key}[#{item}]" }
66
+ end.sort
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/policy_info'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This is the base class for all matchers in the Pundit Matchers library.
8
+ class BaseMatcher
9
+ # Error message when an ambiguous negated matcher is used.
10
+ AMBIGUOUS_NEGATED_MATCHER_ERROR = <<~MSG
11
+ `expect().not_to %<name>s` is not supported since it creates ambiguity.
12
+ MSG
13
+
14
+ private
15
+
16
+ attr_reader :policy_info
17
+
18
+ def setup_policy_info!(policy)
19
+ @policy_info = Pundit::Matchers::Utils::PolicyInfo.new(policy)
20
+ end
21
+
22
+ def user_message
23
+ " for '#{policy_info.user}'"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy forbids all actions.
8
+ class ForbidAllActionsMatcher < BaseMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ 'forbid all actions'
14
+ end
15
+
16
+ # Checks if the given policy forbids all actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy forbids all actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ policy_info.permitted_actions.empty?
24
+ end
25
+
26
+ # Raises a NotImplementedError
27
+ # @raise NotImplementedError
28
+ # @return [void]
29
+ def does_not_match?(_policy)
30
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'forbid_all_actions')
31
+ end
32
+
33
+ # Returns a failure message if the matcher fails.
34
+ #
35
+ # @return [String] Failure message.
36
+ def failure_message
37
+ message = +"expected '#{policy_info}' to forbid all actions,"
38
+ message << " but permitted #{policy_info.permitted_actions}"
39
+ message << user_message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy forbids only the expected actions.
8
+ class ForbidOnlyActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ "forbid only #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy forbids only the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy forbids only the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = policy_info.forbidden_actions - expected_actions
25
+ @extra_actions = policy_info.permitted_actions & expected_actions
26
+
27
+ actual_actions.empty? && extra_actions.empty?
28
+ end
29
+
30
+ # Raises a NotImplementedError
31
+ # @raise NotImplementedError
32
+ # @return [void]
33
+ def does_not_match?(_policy)
34
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'forbid_only_actions')
35
+ end
36
+
37
+ # The failure message when the expected actions and the forbidden actions do not match.
38
+ #
39
+ # @return [String] A failure message when the expected actions and the forbidden actions do not match.
40
+ def failure_message
41
+ message = +"expected '#{policy_info}' to forbid only #{expected_actions},"
42
+ message << " but forbade #{actual_actions}" unless actual_actions.empty?
43
+ message << extra_message unless extra_actions.empty?
44
+ message << user_message
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :actual_actions, :extra_actions
50
+
51
+ def extra_message
52
+ if actual_actions.empty?
53
+ " but permitted #{extra_actions}"
54
+ else
55
+ " and permitted #{extra_actions}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits or forbids the expected actions.
8
+ class PermitActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ "permit #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy permits the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = expected_actions.reject do |action|
25
+ policy.public_send(:"#{action}?")
26
+ end
27
+
28
+ actual_actions.empty?
29
+ end
30
+
31
+ # Checks if the given policy forbids the expected actions.
32
+ #
33
+ # @param policy [Object] The policy to test.
34
+ # @return [Boolean] True if the policy forbids the expected actions, false otherwise.
35
+ def does_not_match?(policy)
36
+ setup_policy_info! policy
37
+ check_actions!
38
+
39
+ @actual_actions = expected_actions.select do |action|
40
+ policy.public_send(:"#{action}?")
41
+ end
42
+
43
+ actual_actions.empty?
44
+ end
45
+
46
+ # Returns a failure message when the expected actions are forbidden.
47
+ #
48
+ # @return [String] A failure message when the expected actions are not forbidden.
49
+ def failure_message
50
+ message = +"expected '#{policy_info}' to permit #{expected_actions},"
51
+ message << " but forbade #{actual_actions}"
52
+ message << user_message
53
+ end
54
+
55
+ # Returns a failure message when the expected actions are permitted.
56
+ #
57
+ # @return [String] A failure message when the expected actions are permitted.
58
+ def failure_message_when_negated
59
+ message = +"expected '#{policy_info}' to forbid #{expected_actions},"
60
+ message << " but permitted #{actual_actions}"
61
+ message << user_message
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :actual_actions
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits all actions.
8
+ class PermitAllActionsMatcher < BaseMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ 'permit all actions'
14
+ end
15
+
16
+ # Checks if the given policy permits all actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits all actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ policy_info.forbidden_actions.empty?
24
+ end
25
+
26
+ # Raises a NotImplementedError
27
+ # @raise NotImplementedError
28
+ # @return [void]
29
+ def does_not_match?(_policy)
30
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'permit_all_actions')
31
+ end
32
+
33
+ # Returns a failure message when the policy does not permit all actions.
34
+ #
35
+ # @return [String] A failure message when the policy does not permit all actions.
36
+ def failure_message
37
+ message = +"expected '#{policy_info}' to permit all actions,"
38
+ message << " but forbade #{policy_info.forbidden_actions}"
39
+ message << user_message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attributes_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits or forbids the mass assignment of the expected attributes.
8
+ class PermitAttributesMatcher < AttributesMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ "permit the mass assignment of #{expected_attributes}"
14
+ end
15
+
16
+ # Checks if the given policy permits the mass assignment of the expected attributes.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits the mass assignment of the expected attributes, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ @actual_attributes = expected_attributes - permitted_attributes(policy)
24
+
25
+ actual_attributes.empty?
26
+ end
27
+
28
+ # Checks if the given policy forbids the mass assignment of the expected attributes.
29
+ #
30
+ # @param policy [Object] The policy to test.
31
+ # @return [Boolean] True if the policy forbids the mass assignment of the expected attributes, false otherwise.
32
+ def does_not_match?(policy)
33
+ setup_policy_info! policy
34
+
35
+ @actual_attributes = expected_attributes & permitted_attributes(policy)
36
+
37
+ actual_attributes.empty?
38
+ end
39
+
40
+ # The failure message when the expected attributes are forbidden.
41
+ #
42
+ # @return [String] A failure message when the expected attributes are not permitted.
43
+ def failure_message
44
+ message = +"expected '#{policy_info}' to permit the mass assignment of #{expected_attributes}"
45
+ message << action_message if options.key?(:action)
46
+ message << ", but forbade the mass assignment of #{actual_attributes}"
47
+ message << user_message
48
+ end
49
+
50
+ # The failure message when the expected attributes are permitted.
51
+ #
52
+ # @return [String] A failure message when the expected attributes are forbidden.
53
+ def failure_message_when_negated
54
+ message = +"expected '#{policy_info}' to forbid the mass assignment of #{expected_attributes}"
55
+ message << action_message if options.key?(:action)
56
+ message << ", but permitted the mass assignment of #{actual_attributes}"
57
+ message << user_message
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :actual_attributes
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits only the expected actions.
8
+ class PermitOnlyActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ "permit only #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy permits only the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits only the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = policy_info.permitted_actions - expected_actions
25
+ @extra_actions = policy_info.forbidden_actions & expected_actions
26
+
27
+ actual_actions.empty? && extra_actions.empty?
28
+ end
29
+
30
+ # Raises a NotImplementedError
31
+ # @raise NotImplementedError
32
+ # @return [void]
33
+ def does_not_match?(_policy)
34
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'permit_only_actions')
35
+ end
36
+
37
+ # The failure message when the expected actions and the permitted actions do not match.
38
+ #
39
+ # @return [String] A failure message when the expected actions and the permitted actions do not match.
40
+ def failure_message
41
+ message = +"expected '#{policy_info}' to permit only #{expected_actions},"
42
+ message << " but permitted #{actual_actions}" unless actual_actions.empty?
43
+ message << extra_message unless extra_actions.empty?
44
+ message << user_message
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :actual_actions, :extra_actions
50
+
51
+ def extra_message
52
+ if actual_actions.empty?
53
+ " but forbade #{extra_actions}"
54
+ else
55
+ " and forbade #{extra_actions}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -3,27 +3,88 @@
3
3
  module Pundit
4
4
  module Matchers
5
5
  module Utils
6
- # Collects all details about given policy class.
6
+ # This class provides methods to retrieve information about a policy class,
7
+ # such as the actions it defines and which of those actions are permitted
8
+ # or forbidden. It also provides a string representation of the policy class name
9
+ # and the user object associated with the policy.
7
10
  class PolicyInfo
11
+ # Error message when policy does not respond to `user_alias`.
12
+ USER_NOT_IMPLEMENTED_ERROR = <<~MSG
13
+ '%<policy>s' does not implement '%<user_alias>s'. You may want to
14
+ configure an alias, which you can do as follows:
15
+
16
+ Pundit::Matchers.configure do |config|
17
+ # Alias for all policies
18
+ config.default_user_alias = :%<user_alias>s
19
+
20
+ # Per-policy alias
21
+ config.user_aliases = { '%<policy>s' => :%<user_alias>s }
22
+ end
23
+ MSG
24
+
8
25
  attr_reader :policy
9
26
 
27
+ # Initializes a new instance of PolicyInfo.
28
+ #
29
+ # @param policy [Class] The policy class to collect details about.
10
30
  def initialize(policy)
11
31
  @policy = policy
32
+ check_user_alias!
33
+ end
34
+
35
+ # Returns a string representation of the policy class name.
36
+ #
37
+ # @return [String] A string representation of the policy class name.
38
+ def to_s
39
+ policy.class.name
40
+ end
41
+
42
+ # Returns the user object associated with the policy.
43
+ #
44
+ # @return [Object] The user object associated with the policy.
45
+ def user
46
+ @user ||= policy.public_send(user_alias)
12
47
  end
13
48
 
49
+ # Returns an array of all actions defined in the policy class.
50
+ #
51
+ # It assumes that actions are defined as public instance methods that end with a question mark.
52
+ #
53
+ # @return [Array<Symbol>] An array of all actions defined in the policy class.
14
54
  def actions
15
- @actions ||= begin
16
- policy_methods = @policy.public_methods - Object.instance_methods
17
- policy_methods.grep(/\?$/).sort.map { |policy_method| policy_method.to_s.delete_suffix('?').to_sym }
55
+ @actions ||= policy_public_methods.grep(/\?$/).sort.map do |policy_method|
56
+ policy_method.to_s.delete_suffix('?').to_sym
18
57
  end
19
58
  end
20
59
 
60
+ # Returns an array of all permitted actions defined in the policy class.
61
+ #
62
+ # @return [Array<Symbol>] An array of all permitted actions defined in the policy class.
21
63
  def permitted_actions
22
- @permitted_actions ||= actions.select { |action| policy.public_send("#{action}?") }
64
+ @permitted_actions ||= actions.select { |action| policy.public_send(:"#{action}?") }
23
65
  end
24
66
 
67
+ # Returns an array of all forbidden actions defined in the policy class.
68
+ #
69
+ # @return [Array<Symbol>] An array of all forbidden actions defined in the policy class.
25
70
  def forbidden_actions
26
- actions - permitted_actions
71
+ @forbidden_actions ||= actions - permitted_actions
72
+ end
73
+
74
+ private
75
+
76
+ def policy_public_methods
77
+ @policy_public_methods ||= policy.public_methods - Object.instance_methods
78
+ end
79
+
80
+ def user_alias
81
+ @user_alias ||= Pundit::Matchers.configuration.user_alias(policy)
82
+ end
83
+
84
+ def check_user_alias!
85
+ return if policy.respond_to?(user_alias)
86
+
87
+ raise ArgumentError, format(USER_NOT_IMPLEMENTED_ERROR, policy: self, user_alias: user_alias)
27
88
  end
28
89
  end
29
90
  end