policy 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Guardfile +4 -4
  3. data/README.md +177 -78
  4. data/config/metrics/flay.yml +1 -1
  5. data/config/metrics/roodi.yml +2 -2
  6. data/lib/policy.rb +52 -33
  7. data/lib/policy/base.rb +122 -0
  8. data/lib/policy/base/and.rb +38 -0
  9. data/lib/policy/base/negator.rb +52 -0
  10. data/lib/policy/base/node.rb +59 -0
  11. data/lib/policy/base/not.rb +42 -0
  12. data/lib/policy/base/or.rb +39 -0
  13. data/lib/policy/base/xor.rb +39 -0
  14. data/lib/policy/cli.rb +8 -3
  15. data/lib/policy/cli/attribute.rb +49 -0
  16. data/lib/policy/cli/locale.erb +1 -2
  17. data/lib/policy/cli/policy.erb +33 -6
  18. data/lib/policy/cli/spec.erb +31 -11
  19. data/lib/policy/follower.rb +54 -94
  20. data/lib/policy/follower/name_error.rb +53 -0
  21. data/lib/policy/follower/policies.rb +104 -0
  22. data/lib/policy/follower/violation_error.rb +60 -0
  23. data/lib/policy/version.rb +2 -2
  24. data/policy.gemspec +2 -3
  25. data/spec/support/composer.rb +28 -0
  26. data/spec/tests/lib/policy/base/and_spec.rb +62 -0
  27. data/spec/tests/lib/policy/base/negator_spec.rb +49 -0
  28. data/spec/tests/lib/policy/base/not_spec.rb +50 -0
  29. data/spec/tests/lib/policy/base/or_spec.rb +62 -0
  30. data/spec/tests/lib/policy/base/xor_spec.rb +73 -0
  31. data/spec/tests/lib/policy/base_spec.rb +123 -0
  32. data/spec/tests/lib/policy/cli/attribute_spec.rb +52 -0
  33. data/spec/tests/{policy → lib/policy}/cli_spec.rb +25 -24
  34. data/spec/tests/lib/policy/follower/name_error_spec.rb +51 -0
  35. data/spec/tests/lib/policy/follower/policies_spec.rb +156 -0
  36. data/spec/tests/lib/policy/follower/violation_error_spec.rb +60 -0
  37. data/spec/tests/lib/policy/follower_spec.rb +153 -0
  38. data/spec/tests/lib/policy_spec.rb +52 -0
  39. metadata +43 -44
  40. data/lib/policy/follower/followed_policies.rb +0 -45
  41. data/lib/policy/follower/followed_policy.rb +0 -104
  42. data/lib/policy/follower/names.rb +0 -29
  43. data/lib/policy/interface.rb +0 -48
  44. data/lib/policy/validations.rb +0 -28
  45. data/lib/policy/violation_error.rb +0 -52
  46. data/spec/features/follower_spec.rb +0 -95
  47. data/spec/tests/policy/follower/followed_policies_spec.rb +0 -87
  48. data/spec/tests/policy/follower/followed_policy_spec.rb +0 -117
  49. data/spec/tests/policy/follower/names_spec.rb +0 -19
  50. data/spec/tests/policy/follower_spec.rb +0 -220
  51. data/spec/tests/policy/interface_spec.rb +0 -83
  52. data/spec/tests/policy/validations_spec.rb +0 -13
  53. data/spec/tests/policy/violation_error_spec.rb +0 -75
  54. data/spec/tests/policy_spec.rb +0 -35
@@ -8,7 +8,6 @@
8
8
  # models:
9
9
  # <%= project.path %>/<%= policy.path %>:
10
10
  # attributes:
11
- # base:
12
- <% attributes.each do |item| -%>
11
+ <% ["base"].concat(attributes.map(&:name)).sort.each do |item| -%>
13
12
  # <%= item %>:
14
13
  <% end -%>
@@ -16,18 +16,45 @@
16
16
 
17
17
  <%= " " * tabs %># Implements the following policy (invariant):
18
18
  <%= " " * tabs %>#
19
- <%= " " * tabs %># @todo: describe the policy
19
+ <%= " " * tabs %># @todo Describe the policy
20
20
  <%= " " * tabs %>#
21
21
  <%= " " * tabs %># @example
22
- <%= " " * tabs %># <%= policy.type %>.new(<%= attributes.join(", ") %>)
22
+ <%= " " * tabs %># <%= project.type %>::<%= policy.type %>.new(
23
+ <% attributes.each do |item| -%>
24
+ <%= " " * tabs %># <%= item.name %>: @todo,
25
+ <% end -%>
26
+ <%= " " * tabs %># )
23
27
  <%= " " * tabs %>#
28
+ <%= " " * tabs %>class <%= policy.const %>
29
+ <%= " " * tabs %> include Policy::Base
30
+
31
+ <%= " " * tabs %> # @!scope class
32
+ <%= " " * tabs %> # @!method new(attributes)
33
+ <%= " " * tabs %> # Creates the policy object with a list of attributes
34
+ <%= " " * tabs %> #
35
+ <%= " " * tabs %> # @param [Hash] attributes
36
+ <%= " " * tabs %> #
37
+ <%= " " * tabs %> # @return [<%= project.type %>::<%= policy.type %>]
38
+ <% if attributes.any? -%>
39
+
40
+ <%= " " * tabs %> # @private
41
+ <%= " " * tabs %> def initialize(<%= attributes.map { |item| "#{ item.name }:" }.join(", ") -%>)
24
42
  <% attributes.each do |item| -%>
25
- <%= " " * tabs %># @param [@todo] <%= item %>
26
- <%= " " * tabs %># @todo: describe the attribute
43
+ <%= " " * tabs %> @<%= item.name %> = <%= item.name %>
44
+ <% end -%>
45
+ <%= " " * tabs %> end
46
+ <% attributes.each do |item| -%>
47
+
48
+ <%= " " * tabs %> # # @!attribute [r] <%= item.name %>
49
+ <%= " " * tabs %> # # @todo Describe the attribute
50
+ <%= " " * tabs %> # #
51
+ <%= " " * tabs %> # # @return [<%= item.type %>]
52
+ <%= " " * tabs %> # attr_reader :<%= item.name %>
53
+ <% end -%>
27
54
  <% end -%>
28
- <%= " " * tabs %>class <%= policy.const %> < Hexx::Policy.new(<%= attributes.map { |item| ":#{ item }" }.join(", ") %>)
29
55
 
30
- <%= " " * tabs %> # Define necessary validations using `validates` or `validate` declarations.
56
+ <%= " " * tabs %> # Define necessary validations using either `validates`
57
+ <%= " " * tabs %> # or `validate` declarations.
31
58
 
32
59
  <%= " " * tabs %>end # class <%= policy.const %>
33
60
  <% policy.namespaces.reverse.each do |item| -%>
@@ -7,24 +7,44 @@ describe <%= project.type %>::<%= policy.type %> do
7
7
 
8
8
  # default attributes for a valid policy
9
9
  <% attributes.each do |item| -%>
10
- # let(:<%= item %>) { @todo }
10
+ # let(:<%= item.name %>) { @todo }
11
11
  <% end -%>
12
12
 
13
- subject { described_class.new <%= attributes.join(", ") %> }
13
+ subject do
14
+ described_class.new(
15
+ <% attributes.each do |item| -%>
16
+ <%= item.name %>: <%= item.name %>,
17
+ <% end -%>
18
+ )
19
+ end
20
+ <% attributes.each do |item| -%>
14
21
 
15
- context "when @todo: describe the context" do
22
+ describe "#<%= item.name %>" do
16
23
 
17
- it { is_expected.to be_valid }
24
+ it "is initialized" do
25
+ expect(subject.<%= item.name %>).to eq <%= item.name %>
26
+ end
18
27
 
19
- end # context
20
- <% attributes.each do |item| -%>
28
+ end # describe #<%= item.name %>
29
+ <% end -%>
21
30
 
22
- context "when <%= item %> @todo" do
31
+ describe "#valid?" do
23
32
 
24
- # let(:<%= item %>) { @todo }
25
- it { is_expected.not_to be_valid }
33
+ context "when @todo: describe the context" do
26
34
 
27
- end # context
28
- <% end -%>
35
+ it { is_expected.to be_valid }
36
+
37
+ end # context
38
+ <% attributes.each do |item| -%>
39
+
40
+ context "when the #<%= item.name %> is @todo" do
41
+
42
+ # before { allow(subject).to receive(:<%= item.name %>) { @todo } }
43
+ it { is_expected.to be_invalid }
44
+
45
+ end # context
46
+ <% end -%>
47
+
48
+ end # describe #valid?
29
49
 
30
50
  end # describe <%= policy.type %>
@@ -2,140 +2,100 @@
2
2
 
3
3
  module Policy
4
4
 
5
- # Adds features for the object to follow external policies
5
+ # Interface for the class that follows policies
6
6
  module Follower
7
7
 
8
- require_relative "follower/names"
9
- require_relative "follower/followed_policy"
10
- require_relative "follower/followed_policies"
11
-
12
- # Methods to be added to the class the module is included to
13
- #
14
- # @private
8
+ # Methods to be added to the follower class
15
9
  module ClassMethods
16
10
 
17
- # @!attribute [r] followed_policies
18
- # The collection of policies to be followed by instances of the class
19
- #
20
- # @return [Policy::Follower::FollowedPolicies]
21
- #
22
- # @private
23
- def followed_policies
24
- @followed_policies ||= FollowedPolicies.new
25
- end
26
-
27
- # Adds a policy to the list of {#followed_policies}
11
+ # Declares a policy to be followed
28
12
  #
29
- # @param [Class] policy
30
- # the policy object klass
31
- # @param [Array<#to_sym>] attributes
32
- # the list of attributes of the instance the policy should be applied to
13
+ # Mutates the object by adding an item to its {#policies}.
33
14
  #
34
- # @option [#to_sym] :as
35
- # the name for the policy to be used for selecting it
36
- # in {#follow_policies!} and {#follow_policies?} methods
15
+ # @param [Symbol] name
37
16
  #
38
- # @return [undefined]
39
- def follow_policy(policy, *attributes, as: nil)
40
- object = FollowedPolicy.new(__policies__, policy, as, *attributes)
41
- followed_policies.add object
17
+ # @return [:follows_policy] the name of the method
18
+ def follows_policy(name)
19
+ policies.add name
20
+
21
+ :follows_policy
42
22
  end
43
23
 
44
- # Changes the namespace for applied policies
24
+ # Declares policies to be followed
45
25
  #
46
- # @example For Policies::Finances::TransferConsistency
47
- # use_policies Policies::Finances do
48
- # apply_policy :TransferConstistency, :debet, :credit
49
- # end
26
+ # Mutates the object by adding items to its {#policies}.
50
27
  #
51
- # @param [Module] namespace
28
+ # @param [Symbol, Array<Symbol>] names
52
29
  #
53
- # @yield the block in the current scope
54
- #
55
- # @return [undefined]
56
- def use_policies(namespace, &block)
57
- @__policies__ = namespace
58
- instance_eval(&block)
59
- ensure
60
- @__policies__ = nil
61
- end
30
+ # @return [:follows_policies] the name of the method
31
+ def follows_policies(*names)
32
+ names.each(&method(:follows_policy))
62
33
 
63
- private
64
-
65
- def __policies__
66
- @__policies__ ||= self
34
+ :follows_policies
67
35
  end
68
36
 
69
- end
37
+ # @!attribute [r] policies
38
+ # The collection of followed policies
39
+ #
40
+ # @return [Policy::Follower::Policies]
41
+ def policies
42
+ @policies ||= Policies.new(self)
43
+ end
70
44
 
71
- # Checks whether an instance meets selected policies
72
- #
73
- # Mutates the object by adding new #errors
74
- #
75
- # @param [Array<#to_sym>] names
76
- # the ordered list of names to select policies by
77
- # when not names selected all policies will be applied
78
- #
79
- # @raise [Policy::ViolationError]
80
- # unless all selected policies has been met
81
- #
82
- # @return [undefined]
83
- def follow_policies!(*names)
84
- followed_policies.apply_to self, *names
85
- rescue ViolationError => error
86
- collect_errors_from(error)
87
- raise
88
45
  end
89
46
 
90
- # Syntax shugar for the {#follow_policies!} with one argument
47
+ # Checks whether an object follows all its policies or subset of policies
91
48
  #
92
- # @param [#to_sym] name
93
- # the name of the policy to follow
49
+ # @overload follow_policies?()
50
+ # Checks whether an object follows all registered policies
94
51
  #
95
- # @raise (see #follow_policies!)
52
+ # @overload follow_policies?(names)
53
+ # Checks whether an object follows given policies
96
54
  #
97
- # @return [undefined]
98
- def follow_policy!(name)
99
- follow_policies! name
100
- end
101
-
102
- # Safely checks whether an instance meets selected policies
55
+ # @param [#to_sym, Array<#to_sym>] names
103
56
  #
104
- # Mutates the object by adding new #errors
57
+ # @raise [Policy::Follower::NameError]
58
+ # if the policy is not registered by name
105
59
  #
106
- # @param (see #follow_policies!)
60
+ # @raise [NoMethodError]
61
+ # if the name not implemented as follower's instance method
62
+ # @raise [ViolationError]
63
+ # if the policy is violated by the follower
107
64
  #
108
- # @return [Boolean]
65
+ # @return [true] if no exception being raised
109
66
  def follow_policies?(*names)
110
- follow_policies!(*names)
67
+ __policies__
68
+ .subset(names) # raises NameError
69
+ .map(&method(:__send__)) # raises NoMethodError
70
+ .each(&method(:__validate_policy__)) # raises ViolationError
111
71
  true
112
- rescue ViolationError
113
- false
114
72
  end
115
73
 
116
- # Syntax shugar for the {#follow_policies?} with one argument
74
+ # Checks whether an object follows given policy
117
75
  #
118
- # @param (see #follow_policy!)
76
+ # @param [#to_sym] name
77
+ # the name of the policy to follow
78
+ #
79
+ # @raise (see #follow_policies?)
119
80
  #
120
81
  # @return (see #follow_policies?)
121
82
  def follow_policy?(name)
122
83
  follow_policies? name
123
84
  end
124
85
 
125
- private
126
-
127
- # @!parse extend Policy::Follower::ClassMethods
128
- # @!parse include ActiveModel::Validations
86
+ # @private
129
87
  def self.included(klass)
130
- klass.extend(ClassMethods).__send__(:include, Validations)
88
+ klass.instance_eval { extend ClassMethods }
131
89
  end
132
90
 
133
- def followed_policies
134
- @followed_policies ||= self.class.followed_policies
91
+ private
92
+
93
+ def __validate_policy__(policy)
94
+ fail ViolationError.new(self, policy) unless policy.valid?
135
95
  end
136
96
 
137
- def collect_errors_from(exception)
138
- exception.messages.each { |text| errors.add :base, text }
97
+ def __policies__
98
+ self.class.policies
139
99
  end
140
100
 
141
101
  end # module Follower
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ module Follower
6
+
7
+ # An exception to be risen when a policy name is not followed by a follower
8
+ class NameError < RuntimeError
9
+
10
+ # @!scope class
11
+ # @!method new(follower, name)
12
+ # Constructs an exception for the follower class and the policy name
13
+ #
14
+ # @param [Class] follower
15
+ # @param [#to_sym] name
16
+ #
17
+ # @return [Policy::Follower::NameError]
18
+ def initialize(follower, name)
19
+ @follower = follower
20
+ @name = name.to_sym
21
+ end
22
+
23
+ # @!attribute [r] follower
24
+ # The follower class that doesn't follow given policy
25
+ #
26
+ # @return [Class]
27
+ attr_reader :follower
28
+
29
+ # @!attribute [r] name
30
+ # The name of the policy that is not followed by the follower
31
+ #
32
+ # @return [String]
33
+ attr_reader :name
34
+
35
+ # The human-readable exception message
36
+ #
37
+ # @return [String]
38
+ def message
39
+ "#{ follower.inspect } hasn't registered the policy \"#{ name }\""
40
+ end
41
+
42
+ # The human-readable description for the exception
43
+ #
44
+ # @return [String]
45
+ def inspect
46
+ "#<#{ self }: #{ message }>"
47
+ end
48
+
49
+ end # class NameError
50
+
51
+ end # module Follower
52
+
53
+ end # module Policy
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ module Follower
6
+
7
+ # Collection of policy names to be followed
8
+ #
9
+ # @api private
10
+ class Policies
11
+ include Enumerable
12
+
13
+ # @!scope class
14
+ # @!method new(follower)
15
+ # Creates the empty collection for the follower class
16
+ #
17
+ # @param [Class] follower
18
+ #
19
+ # @return [Policy::Follower::Policies] the collection object
20
+ def initialize(follower)
21
+ @follower = follower
22
+ @names = []
23
+ end
24
+
25
+ # Adds the policy name to the list of followed policies
26
+ #
27
+ # @example
28
+ # Policies.new.add("some_policy")
29
+ #
30
+ # @param [#to_sym] policy
31
+ #
32
+ # @return [self] itself
33
+ def add(policy)
34
+ name = policy.to_sym
35
+ names << name unless names.include? name
36
+ self
37
+ end
38
+
39
+ # Checks whether the name is included to the {#names}
40
+ #
41
+ # @param [Symbol] name
42
+ #
43
+ # @raise [Policy::Followers::NameError] if the name not registered yet
44
+ #
45
+ # @return [true]
46
+ def include?(name)
47
+ fail(NameError.new(follower, name)) unless names.include?(name)
48
+ true
49
+ end
50
+
51
+ # Returns the subset of current collection
52
+ #
53
+ # @overload subset()
54
+ # @return [self] itself
55
+ #
56
+ # @overload subset(*names)
57
+ # @example
58
+ # policies = Policies.new(follower, %i(foo bar baz))
59
+ # policies.subset(:baz, :foo)
60
+ # # => #<Policies @names=[:baz, :foo]>
61
+ #
62
+ # @param [Symbol, Array<Symbol>] names
63
+ #
64
+ # @raise [Policy::Follower::NameError]
65
+ # if subset contains unregistered policies
66
+ #
67
+ # @return [Policy::Follower::Policies]
68
+ def subset(*names)
69
+ return self unless names.flatten.any?
70
+ policies = self.class.new(follower)
71
+ valid(names).each(&policies.method(:add))
72
+
73
+ policies
74
+ end
75
+
76
+ # Iterates through names of the policies
77
+ #
78
+ # @return [Enumerator]
79
+ #
80
+ # @yieldparam [Symbol] the name of the registered policy
81
+ def each
82
+ return to_enum unless block_given?
83
+ names.each { |name| yield(name) }
84
+ end
85
+
86
+ # @!attribute [r] follower
87
+ # The class that follows policies
88
+ #
89
+ # @return [Class]
90
+ attr_reader :follower
91
+
92
+ private
93
+
94
+ attr_reader :names
95
+
96
+ def valid(names)
97
+ names.flatten.map(&:to_sym).select(&method(:include?))
98
+ end
99
+
100
+ end # class Policies
101
+
102
+ end # module Follower
103
+
104
+ end # module Policy