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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11f8a87bec42ea9a04af224a56604ff2d690a8da
4
- data.tar.gz: f8d9626c4298248ef86f91c7720afb8c26f976d0
3
+ metadata.gz: dfc9740968e7733a76015392220aff44505d85e3
4
+ data.tar.gz: bbfb0d67cbbff03aa344d1c8f2ba69dc3fd4f16d
5
5
  SHA512:
6
- metadata.gz: 4a76b932814bdbf8a70eb7e5ac55dc9ad1d82f93ae8c9d6151ebdceeeea0979e616c8f70637b7a777f50ee1e7ad30013f039d33fd5329665d0e128a06f8af8bd
7
- data.tar.gz: 532d978cd83c62e7374d46a00f44c6c4034f9df1e3cccbe386d4f43a1cf60fc926580dc33996bd2892a2e4ca13accd265e256bfabf828c855ac7ad4472be8bb9
6
+ metadata.gz: 811e7374d0b8c41cec609dd9adb42eb5cedde61e391bed53cdd6cd4b1e2c72674c5207a3d9cfe85fac99b28aa52ead898fc737625629dd07cdd8c3f6ba8bf4dc
7
+ data.tar.gz: c75267c516e6da655b80980ca2437878c858d33b45ac9520bccdfea981e0de2cb96e80327e612c8fbcd6bebfc8b05eedb170628deaf09c62f778267dda2228af
data/Guardfile CHANGED
@@ -6,12 +6,12 @@ guard :rspec, cmd: "bundle exec rspec" do
6
6
 
7
7
  watch("lib/policy.rb") { "spec" }
8
8
 
9
- watch("lib/policy/cli/*.*") { "spec/tests/policy/cli_spec.rb" }
9
+ watch(%r{^lib/policy/cli/}) { "spec/tests/policy/cli_spec.rb" }
10
10
 
11
- watch(/^lib(.+)\.rb$/) do |m|
12
- "spec/tests#{ m[1] }_spec.rb"
11
+ watch(/^(bin|lib)\/(.+)\.rb$/) do |m|
12
+ "spec/tests/#{ m[1] }/#{ m[2] }_spec.rb"
13
13
  end
14
14
 
15
- watch(/^spec.+_spec\.rb$/)
15
+ watch(%r{^spec/tests/.+_spec\.rb$})
16
16
 
17
17
  end # guard :rspec
data/README.md CHANGED
@@ -17,28 +17,36 @@ Policy
17
17
 
18
18
  A tiny library to implement a **Policy Object pattern**.
19
19
 
20
+ **NOTE** the gem was re-written from scratch in v2.0.0 (see Changelog section below)
21
+
22
+ Introduction
23
+ ------------
24
+
20
25
  The gem was inspired by:
21
- * the CodeClimate's blog post "[7 ways to decompose fat ActiveRecord module]"
22
- * the part "How to Model Less Obvious Kinds of Concept" from the "[Domain-Driven Design]" by Eric Evans.
26
+ * the CodeClimate's blog post "[7 ways to decompose fat ActiveRecord module]".
27
+ * the Chapter 10 of the book "[Domain-Driven Design]" by Eric Evans.
28
+
29
+ A **Policy Object** (assertion, invariant) encapsulates a business rule in isolation from objects (such as entities or services) following it.
23
30
 
24
- A **Policy Object** encapsulates a business rule in isolation from objects (such as entities or services) following it.
31
+ Policy Objects can be combined by logical operators `and`, `or`, `xor`, `not` to provide complex policies.
25
32
 
26
- This separation provides a number of benefits:
33
+ This approach gives a number of benefits:
27
34
 
28
35
  * It makes business rules **explicit** instead of spreading and hiding them inside application objects.
29
- * It makes the rules **reusable** in various context (think of the *transaction consistency* both in bank transfers and cach machine withdrawals).
30
- * It allows definition of rules for **numerous attributes** that should correspond to each other in some way.
31
- * It makes complex rules **testable** in isolation from even more complex objects.
36
+ * It allows definition of rules for **numerous attributes** at once that should correspond to each other in some way.
37
+ * It makes the rules **simple** and **reusable** in various context and combinations.
38
+ * It makes complex rules **testable** in isolation from their parts.
32
39
 
33
40
  [7 ways to decompose fat ActiveRecord module]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
34
41
  [Domain-Driven Design]: http://www.amazon.com/dp/B00794TAUG/
35
42
 
36
- # Installation
43
+ Installation
44
+ ------------
37
45
 
38
46
  Add this line to your application's Gemfile:
39
47
 
40
48
  ```ruby
41
- gem "policy"
49
+ gem "policy", ">= 1.0"
42
50
  ```
43
51
 
44
52
  And then execute:
@@ -53,39 +61,43 @@ Or install it yourself as:
53
61
  $ gem install policy
54
62
  ```
55
63
 
56
- # Usage
64
+ Usage
65
+ -----
57
66
 
58
- ## The Model for Illustration
67
+ ### The Model for Illustration
59
68
 
60
69
  Suppose an over-simplified model of bank account transactions and account-to-account transfers.
61
70
 
62
71
  ```ruby
63
- # The account transaction (either enrollment or witdrawal)
64
- class Transaction < Struct.new(:sum); end
72
+ # The account has a limit
73
+ class Account < Struct.new(:owner, :limit); end
74
+
75
+ # The transaction belongs to account and has a sum (< 0 for withdrawals)
76
+ class Transaction < Struct.new(:account, :sum); end
65
77
 
66
78
  # The transfer, connecting two separate transactions
67
- # (maybe this isn't an optimal model, but helpful for the subject)
68
79
  class Transfer < Struct.new(:withdrawal, :enrollment); end
69
80
  ```
70
81
 
71
- What we need is to apply the simple policy (invariant):
82
+ What we need is to apply set of policies:
72
83
 
73
84
  **The sum of withdrawal's and enrollment's sums should be 0.**
85
+ **The sum of withdrawal doesn't exceed the accounts' limit.**
86
+ **The sum of transfers between client's own accounts can exceed the limit.**
74
87
 
75
88
  Let's do it with Policy Objects!
76
89
 
77
- ## Policy Declaration
78
-
79
- Define policies with a list of necessary attributes like using [Struct].
90
+ ### Policy Declaration
80
91
 
81
- Tnen use [ActiveModel::Validations] methods to describe its rules:
92
+ Define policies with `Policy::Base` module included. Tnen use [ActiveModel::Validations] methods to describe its rules:
82
93
 
83
94
  ```ruby
84
95
  # An arbitrary namespace for financial policies
85
- module Policies::Financial
96
+ module Policies
86
97
 
87
98
  # Withdrawal from one account should be equal to enrollment to another
88
- class Consistency < Policy.new(:withdrawal, :enrollment)
99
+ class Consistency < Struct.new(:withdrawal, :enrollment)
100
+ include Policy::Base
89
101
 
90
102
  validates :withdrawal, :enrollment, presence: true
91
103
  validates :total_sum, numericality: { equal_to: 0 }
@@ -96,119 +108,204 @@ module Policies::Financial
96
108
  withdrawal.sum + enrollment.sum
97
109
  end
98
110
  end
99
- end
100
- ```
101
111
 
102
- Note a policy knows nothing about the complex nature of its attributes until their quack like transactions with `#sum` method defined.
112
+ # The sum of withdrawal doesn't exceed the accounts' limit
113
+ class Limited < Struct.new(:withdrawal)
114
+ include Policy::Base
103
115
 
104
- ### Scaffolding a Policy
116
+ validate :not_exceeds_the_limit
105
117
 
106
- You can scaffold the policy with its specification and necessary translations using the generator:
118
+ private
107
119
 
108
- ```
109
- policy new
110
- ```
120
+ def not_exceeds_the_limit
121
+ return if withdrawal.sum + withdrawal.limit > 0
122
+ errors.add :base, :exceeds_the_limit
123
+ end
124
+ end
111
125
 
112
- For a list of available options call the generator with a `-h` option:
126
+ # The transfer is made between client's own accounts
127
+ class InternalTransfer < Struct.new(:withdrawal, :enrollment)
128
+ include Policy::Base
113
129
 
114
- ```
115
- policy new -h
130
+ validate :the_same_client
131
+
132
+ private
133
+
134
+ def the_same_client
135
+ return if withdrawal.account.owner == enrollment.account.owner
136
+ errors.add :base, :different_owners
137
+ end
138
+ end
139
+
140
+ end
116
141
  ```
117
142
 
118
143
  [Struct]: http://ruby-doc.org//core-2.2.0/Struct.html
119
144
  [ActiveModel::Validations]: http://apidock.com/rails/ActiveModel/Validations
120
145
 
121
- ## Following a Policy
146
+ ### Combining Policies
122
147
 
123
- Include the `Policy::Follower` module to the class and apply policies to corresponding attributes with `follow_policy` **class** method.
148
+ Use `and`, `or`, `xor` instance methods to provide complex policies from elementary ones.
149
+
150
+ You can use factory methods:
124
151
 
125
152
  ```ruby
126
- class Transfer < Struct.new(:withdrawal, :enrollment)
127
- include Policy::Follower # also includes ActiveModel::Validations
153
+ module Policies
154
+
155
+ module LimitedOrInternal
156
+ def self.new(withdrawal, enrollment)
157
+ InternalTransfer.new(withdrawal, enrollment).or Limited.new(withdrawal)
158
+ end
159
+ end
128
160
 
129
- follow_policy Policies::Financial::Consistency, :withdrawal, :enrollment
130
161
  end
131
162
  ```
132
163
 
133
- The order of attributes should correspond to the policy definition.
134
-
135
- You can swap attributes (this is ok for our example)...
164
+ As an alternative to instance methods, use the `Policy` module's methods:
136
165
 
137
166
  ```ruby
138
- follow_policy Policies::Financial::Consistency, :enrollment, :withdrawal
167
+ def self.new(withdrawal, enrollment)
168
+ Policy.or(
169
+ InternalTransfer.new(withdrawal, enrollment),
170
+ Limited.new(withdrawal)
171
+ )
172
+ end
139
173
  ```
140
174
 
141
- ...or use the same attribute several times when necessary (not in our example, though):
175
+ To provide negation use `and.not`, `or.not`, `xor.not` syntax:
142
176
 
143
177
  ```ruby
144
- follow_policy Policies::Financial::Consistency, :withdrawal, :withdrawal
178
+ first_policy.and.not(second_policy, third_policy)
179
+
180
+ # this is equal to:
181
+ Policy.and(first_policy, Policy.not(second_policy), Policy.not(third_policy))
145
182
  ```
146
183
 
147
- Applied policies can be grouped by namespaces (useful when the object should follow many policies):
184
+ Policies can composed at any number of levels.
185
+
186
+ ### Following Policies
187
+
188
+ Include the `Policy::Follower` module to the policies follower class.
189
+
190
+ Use the **class** method `.follows_policies` to declare policies (like ActiveModel::Validations `.validate` method does).
148
191
 
149
192
  ```ruby
150
- use_policies Policies::Financial do
151
- follow_policy :Consistency, :withdrawal, :enrollment
193
+ class Transfer < Struct.new(:withdrawal, :enrollment)
194
+ include Policy::Follower
195
+
196
+ follows_policies :consistent, :limited_or_internal
197
+
198
+ private
199
+
200
+ def consistent
201
+ Policies::Consistency.new(withdrawal, enrollment)
202
+ end
203
+
204
+ def limited_or_internal
205
+ Policies::LimitedOrInternal.new(withdrawal, enrollment)
206
+ end
207
+
152
208
  end
153
209
  ```
154
210
 
155
- ## Policies Verification
156
-
157
- To verify object use `#follow_policies?` or `#follow_policies!` **instance** methods.
211
+ Surely, you can skip creating `LimitedOrInternal` builder and combine policies for current class only:
158
212
 
159
213
  ```ruby
160
- Transaction = Struct.new(:account, :sum)
161
- withdrawal = Transaction.new(account_1, -100)
162
- enrollment = Transaction.new(account_2, 1000)
163
-
164
- transfer = Transfer.new withdrawal, enrollment
214
+ def limited_or_internal
215
+ limited.or internal
216
+ end
165
217
 
166
- transfer.follow_policies?
167
- # => false
218
+ def limited
219
+ Policies::Limited.new(withdrawal)
220
+ end
168
221
 
169
- transfer.follow_policies!
170
- # => raises <Policy::ViolationError>
222
+ def internal
223
+ Policies::Internal.new(withdrawal, enrollment)
224
+ end
171
225
  ```
172
226
 
173
- The policies are verified one-by-one until the first break - in just the same order they were declared.
227
+ [builder]: http://sourcemaking.com/design_patterns/builder
174
228
 
175
- ### Asyncronous Verification
229
+ ### Checking Policies
176
230
 
177
- Define names for policies using `as:` option. The names should be unique in the class' scope:
231
+ Use the **instance** method `follow_policies?` to check whether an instance follows policies.
232
+
233
+ The method checks all policies and **raises** the `Policy::ViolationError` when the first followed policy is broken.
178
234
 
179
235
  ```ruby
180
- class Transfer < Struct.new(:withdrawal, :enrollment)
181
- include Policy::Follower
236
+ transfer = Transfer.new(
237
+ Transaction.new(Account.new("Alice", 50), -100),
238
+ Transaction.new(Account.new("Bob", 50), 100)
239
+ )
182
240
 
183
- use_policies Policies::Financial do
184
- follow_policy :Consistency, :withdrawal, :enrollment, as: :consistency
185
- end
186
- end
241
+ transfer.follow_policies?
242
+ # => <Policy::ViolationError ... > because Alice's limit of 50 is exceeded
187
243
  ```
188
244
 
189
- Check policies by names (you can also use singular forms `follow_policy?` and `follow_policy!`):
245
+ The method doesn't mutate the follower. It collects errors inside the exception `#errors` method, not the follower's one.
190
246
 
191
247
  ```ruby
192
- # Checks only consistency and skips all other policies
193
- transfer.follow_policy? :consistency
194
- transfer.follow_policy! :consistency
248
+ begin
249
+ transfer.follow_policies?
250
+ rescue ViolationError => err
251
+ err.errors
252
+ end
195
253
  ```
196
254
 
197
- The set of policies can be checked at once:
255
+ You can check subset of policies by calling the method with policy names:
198
256
 
199
257
  ```ruby
200
- transaction.follow_policies? :consistency, ...
258
+ transfer.follow_policies? :consistent
259
+ # passes because the transfer is consistent: -100 + 100 = 0
260
+ # this doesn't check the :limited_or_internal policy
261
+ ```
262
+
263
+ The method ignores policies, not declared by `.follows_policies` class method.
264
+
265
+ The method has singular alias `follow_policy?(name)` that accepts one argument.
266
+
267
+ Scaffolding
268
+ -----------
269
+
270
+ You can scaffold the policy with its specification and necessary translations using the generator:
271
+
201
272
  ```
273
+ policy new consistency -n policies financial -a withdrawal enrollment -l fr de
274
+ ```
275
+
276
+ For a list of available options call the generator with an `-h` option:
277
+
278
+ ```
279
+ policy new -h
280
+ ```
281
+
282
+ Changelog
283
+ ---------
284
+
285
+ Version 2 was redesigned and rewritten from scratch. The main changes are:
286
+
287
+ * Instead of building policy with a `Policy.new` method, it is now created by including the `Policy::Base` module.
288
+
289
+ In the previous version building a policy was needed to define an order of policy attributes. Now the definition of policy attributes is not the responsibility of the gem.
290
+
291
+ * Instead of generating policies in a class scope (in the ActiveModel `validates` style), the `.follows_policy` refers to followers' instance methods (in the ActiveModel `validate` style).
292
+
293
+ This allows combining policy objects with logical expressions. Policies themselves becames more DRY, granular and testable in isolation.
294
+
295
+ * Instead of mutating the follower, `follow_policy?` method raises an exception.
296
+
297
+ This allows follower to be immutable (frozen). The follower doesn't need to be messed with `ActiveModule::Validations` at all.
202
298
 
203
- Now the policies are verified one-by-one in **given order** (it may differ from the order of policies declaration) until the first break.
299
+ This approach makes `follow_policy!` method unnecessary.
204
300
 
205
- # Compatibility
301
+ Compatibility
302
+ -------------
206
303
 
207
304
  Tested under rubies, compatible with MRI 2.0+:
208
305
 
209
306
  * MRI rubies 2.0+
210
307
  * Rubinius 2+ (2.0+ mode)
211
- * JRuby head (2.0+ mode)
308
+ * JRuby 9000 (2.0+ mode)
212
309
 
213
310
  Rubies with API 1.9 are not supported.
214
311
 
@@ -220,7 +317,8 @@ Uses [RSpec] 3.0+ for testing and [hexx-suit] for dev/test tools collection.
220
317
  [hexx-suit]: https://github.com/nepalez/hexx-suit/
221
318
  [ActiveModel::Validations]: http://apidock.com/rails/v3.1.0/ActiveModel/Validations
222
319
 
223
- # Contributing
320
+ Contributing
321
+ ------------
224
322
 
225
323
  * Fork the project.
226
324
  * Read the [STYLEGUIDE](config/metrics/STYLEGUIDE).
@@ -232,6 +330,7 @@ Uses [RSpec] 3.0+ for testing and [hexx-suit] for dev/test tools collection.
232
330
  in a commit by itself I can ignore when I pull)
233
331
  * Send me a pull request. Bonus points for topic branches.
234
332
 
235
- # License
333
+ License
334
+ -------
236
335
 
237
336
  See [MIT LICENSE](LICENSE).
@@ -1,2 +1,2 @@
1
1
  ---
2
- minimum_score: 5
2
+ minimum_score: 9
@@ -7,9 +7,9 @@ ClassNameCheck:
7
7
  pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/
8
8
  ClassVariableCheck:
9
9
  CyclomaticComplexityBlockCheck:
10
- complexity: 2
10
+ complexity: 3
11
11
  CyclomaticComplexityMethodCheck:
12
- complexity: 2
12
+ complexity: 3
13
13
  EmptyRescueBodyCheck:
14
14
  ForLoopCheck:
15
15
  MethodLineCountCheck:
@@ -1,40 +1,59 @@
1
1
  # encoding: utf-8
2
2
 
3
- require "adamantium"
3
+ require "active_model"
4
4
 
5
- # Policy Object builder
6
- #
7
- # @!parse include Policy::Interface
5
+ require_relative "policy/version"
6
+
7
+ require_relative "policy/base"
8
+ require_relative "policy/base/node"
9
+ require_relative "policy/base/and"
10
+ require_relative "policy/base/or"
11
+ require_relative "policy/base/xor"
12
+ require_relative "policy/base/not"
13
+ require_relative "policy/base/negator"
14
+
15
+ require_relative "policy/follower"
16
+ require_relative "policy/follower/name_error"
17
+ require_relative "policy/follower/policies"
18
+ require_relative "policy/follower/violation_error"
19
+
20
+ # The namespace for the code of the 'policy' gem
8
21
  module Policy
9
22
 
10
- require_relative "policy/version"
11
- require_relative "policy/validations"
12
- require_relative "policy/violation_error"
13
- require_relative "policy/interface"
14
- require_relative "policy/follower"
15
-
16
- class << self
17
-
18
- # Builds a base class for the policy object with some attributes
19
- #
20
- # @example
21
- # class TransactionPolicy < Policy.new(:debet, :credit)
22
- # end
23
- #
24
- # @param [Array<Symbol>] attributes
25
- # names for the policy object attributes
26
- #
27
- # @return [Struct]
28
- def new(*attributes)
29
- Struct.new(*attributes) do
30
- include Interface
31
-
32
- def self.name
33
- "Policy"
34
- end
35
- end
36
- end
37
-
38
- end # Policy singleton class
23
+ # Builds a composite policy by applying method AND to policies
24
+ #
25
+ # @param [Policy::Base, Array<Policy::Base>] policies
26
+ #
27
+ # @return [Policy::Base]
28
+ def self.and(*policies)
29
+ Base::And.new(*policies)
30
+ end
31
+
32
+ # Builds a composite policy by applying method OR to policies
33
+ #
34
+ # @param [Policy::Base, Array<Policy::Base>] policies
35
+ #
36
+ # @return [Policy::Base]
37
+ def self.or(*policies)
38
+ Base::Or.new(*policies)
39
+ end
40
+
41
+ # Builds a composite policy by applying method XOR to policies
42
+ #
43
+ # @param [Policy::Base, Array<Policy::Base>] policies
44
+ #
45
+ # @return [Policy::Base]
46
+ def self.xor(*policies)
47
+ Base::Xor.new(*policies)
48
+ end
49
+
50
+ # Builds the negation of policy
51
+ #
52
+ # @param [Policy::Base] policy
53
+ #
54
+ # @return [Policy::Base]
55
+ def self.not(policy)
56
+ Base::Not.new(policy)
57
+ end
39
58
 
40
59
  end # module Policy