policy 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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