tram-policy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7444c114cfc1974d6a3879cbcde499e7a2e5c93d
4
+ data.tar.gz: 70cc15eee36c5a8d119651e417b35af6d7d2f37d
5
+ SHA512:
6
+ metadata.gz: b106e053fba00768a53c69622603477cd02ac022b0d67c0a8a15c6b7300216778ebbbb328b37e5d974f6b76e89044a1814e9e5428334e8ff93594a1eb01d4260
7
+ data.tar.gz: 62c9b7a44a394c02be30fd2fcb4153b595dbca4d80d941f55b8944c3520ec392e247cf1a57af1dfec58b47e50d0bc865f44f7d59298f8b982da3ad4c9353f53d
data/.codeclimate.yml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ engines:
3
+ rubocop:
4
+ enabled: true
5
+ duplication:
6
+ enabled: true
7
+ config:
8
+ languages:
9
+ - ruby
10
+ exclude_paths:
11
+ - "benchmarks/**/*"
12
+ - "spec/**/*"
13
+ ratings:
14
+ paths:
15
+ - "lib/**/*"
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /*.gem
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ --warnings
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ ---
2
+ AllCops:
3
+ DisplayCopNames: true
4
+ DisplayStyleGuide: true
5
+ StyleGuideCopsOnly: true
6
+ TargetRubyVersion: 2.4
7
+
8
+ Lint/AmbiguousBlockAssociation:
9
+ Enabled: false
10
+
11
+ Style/FileName:
12
+ Exclude:
13
+ - lib/tram-policy.rb
14
+
15
+ Style/PerlBackrefs:
16
+ Enabled: false
17
+
18
+ Style/RaiseArgs:
19
+ EnforcedStyle: compact
20
+
21
+ Style/StringLiterals:
22
+ EnforcedStyle: double_quotes
data/.travis.yml ADDED
@@ -0,0 +1,27 @@
1
+ ---
2
+ language: ruby
3
+ sudo: false
4
+ cache: bundler
5
+ bundler_args: --without benchmarks tools
6
+ script:
7
+ - bundle exec rake spec
8
+ rvm:
9
+ - 2.2
10
+ - 2.3.0
11
+ - 2.4.0
12
+ - jruby-9000
13
+ - rbx-2
14
+ - rbx-3
15
+ - ruby-head
16
+ env:
17
+ global:
18
+ - JRUBY_OPTS='--dev -J-Xmx1024M'
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: rbx-2
22
+ - rvm: rbx-3
23
+ - rvm: ruby-head
24
+ - rvm: jruby-head
25
+ include:
26
+ - rvm: jruby-head
27
+ before_install: gem install bundler --no-ri --no-rdoc
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.0.1] - [2017-04-18]
10
+ This is a first public release (@nepalez, @charlie-wasp, @JewelSam, @sergey-chechaev)
11
+
12
+ [Unreleased]: https://github.com/tram-rb/tram-policy
13
+ [0.0.1]: https://github.com/tram-rb/tram-policy/releases/tag/v0.0.1
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem "pry", platform: :mri
7
+ gem "pry-byebug", platform: :mri
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Evil Martians, Andrew Kozin (nepalez), Viktor Sokolov (gzigzigzeo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # Tram::Policy
2
+
3
+ Policy Object Pattern
4
+
5
+ <a href="https://evilmartians.com/">
6
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
7
+
8
+ [![Gem Version][gem-badger]][gem]
9
+ [![Build Status][travis-badger]][travis]
10
+ [![Dependency Status][gemnasium-badger]][gemnasium]
11
+ [![Code Climate][codeclimate-badger]][codeclimate]
12
+ [![Inline docs][inch-badger]][inch]
13
+
14
+ ## Intro
15
+
16
+ Policy objects are responsible for context-related validation of objects, or mixes of objects. Here **context-related** means a validation doesn't check whether an object is valid by itself, but whether it is valid for some purpose (context). For example, we could ask if some article is ready (valid) to be published, etc.
17
+
18
+ There are several well-known interfaces exist for validation like [ActiveModel::Validations][active-model-validation], or its [ActiveRecord][active-record-validation] extension in Rails, or PORO [Dry::Validation][dry-validation]. All of them focus on providing rich DSL-s for **validation rules**.
19
+
20
+ **Tram::Policy** follows another approach -- it uses simple Ruby methods for validation, but focuses on building both *customizable* and *composable* results of validation, namely their errors.
21
+
22
+ - By **customizable** we mean adding any number of *tags* to validation error -- to allow filtering and sorting validation results.
23
+ - By **composable** we mean a possibility to merge errors provided by one policy/validator to another, for building nested sets of well-focused policies.
24
+
25
+ Keeping this reasons in mind, let's go to some examples.
26
+
27
+ ## Synopsis
28
+
29
+ The gem uses [Dry::Initializer][dry-initializer] interface for defining params and options for policy object instanses:
30
+
31
+ ```ruby
32
+ require "tram-policy"
33
+
34
+ class Article::ReadinessPolicy < Tram::Policy
35
+ # required param for article to validate
36
+ param :article
37
+
38
+ # memoized attributes of the article (you can set them explicitly in specs)
39
+ option :title, proc(&:to_s), default: -> { article.title }
40
+ option :subtitle, proc(&:to_s), default: -> { article.subtitle }
41
+ option :text, proc(&:to_s), default: -> { article.text }
42
+
43
+ # define what methods and in what order we should use to validate an article
44
+ validate :title_presence
45
+ validate :subtitle_presence
46
+ validate :text_presence
47
+
48
+ private
49
+
50
+ def title_presence
51
+ return unless title.empty?
52
+ # Adds an error with a message and a set of additional tags
53
+ # You can use any tags, not only an attribute/field like in ActiveModel
54
+ errors.add "Title is empty", field: "title", level: "error"
55
+ end
56
+
57
+ def subtitle_presence
58
+ return unless subtitle.empty?
59
+ # Notice that we can set another level
60
+ errors.add "Subtitle is empty", field: "subtitle", level: "warning"
61
+ end
62
+
63
+ def text_presence
64
+ return unless text.empty?
65
+ # Adds an error with a translated message. All fields are available
66
+ # both as error tags, and I18n translation options
67
+ errors.add :empty_text, field: "text", level: "error"
68
+ end
69
+ end
70
+ ```
71
+
72
+ Because validation is the only responsibility of a policy, we don't need to call it explicitly. Policy initializer will perform all the checks immediately, memoizing the results into `errors` array. The methods `#valid?`, `#invalid?` and `#validate!` just check those `#errors`.
73
+
74
+ You can treat an instance of policy object as immutable.
75
+
76
+ ```ruby
77
+ article = Article.new title: "A wonderful article", subtitle: "", text: ""
78
+ policy = Article::ReadinessPolicy[article] # syntax sugar for constructor `new`
79
+
80
+ # Simple checks
81
+ policy.errors.any? # => true
82
+ policy.valid? # => false
83
+ policy.invalid? # => true
84
+ policy.validate! # raises Tram::Policy::ValidationError
85
+
86
+ # Look at errors closer
87
+ policy.errors.count # => 2 (no subtitle, no text)
88
+ policy.errors.filter { |error| error.tags[:level] == "error" }.count # => 1
89
+ policy.errors.filter { |error| error.level == "error" }.count # => 1
90
+
91
+ # Error messages are already added under special key :message
92
+ policy.errors.map(&:message) # => ["Subtitle is empty", "Error translation for missed text"]
93
+
94
+ # A shortcut
95
+ policy.messages # => ["Subtitle is empty", "Error translation for missed text"]
96
+
97
+ # More verbose strings
98
+ policy.full_messages
99
+ # => [
100
+ # 'Subtitle is empty: {"field":"subtitle", "level":"warning"}'
101
+ # 'Error translation for missed text: {"field":"text", "level":"error"}'
102
+ # ]
103
+
104
+ # You can use tags in checkers -- to add condition for errors to ignore
105
+ policy.valid? { |error| !%w(warning error).include? error.level } # => false
106
+ policy.valid? { |error| error.level != "disaster" } # => true
107
+
108
+ # Notice the `invalid` takes a block with definitions for errors to count (not ignore)
109
+ policy.invalid? { |error| %w(warning error).include? error.level } # => true
110
+ policy.invalid? { |error| error.level == "disaster" } # => false
111
+
112
+ policy.validate! { |error| error.level != "disaster" } # => nil (seems ok)
113
+ ```
114
+
115
+ You can use errors in composition of policies:
116
+
117
+ ```ruby
118
+ class Article::PublicationPolicy < Tram::Policy
119
+ param :article
120
+ option :selected, proc { |value| !!value } # enforce booleans
121
+
122
+ validate :article_readiness
123
+ validate :article_selection
124
+
125
+ private
126
+
127
+ def article_readiness
128
+ # Collects errors tagged by level: "error" from "nested" policy
129
+ others = Article::ReadinessPolicy[article].errors.by_tags(level: "error")
130
+
131
+ # Merges collected errors to the current ones.
132
+ # New errors are tagged by source: "readiness".
133
+ # Notice the block takes _hashified_ errors.
134
+ errors.merge(others) { |hash| hash[:source] = "readiness" }
135
+ end
136
+
137
+ def article_selection
138
+ errors.add "Not selected", field: "selected", level: "info" unless selected
139
+ end
140
+ end
141
+ ```
142
+
143
+ As mentioned above, sending a symbolic key to the `errors#add` means the key should be translated by [I18n][i18n]. The only magic under the hood is that a scope for the translation is taken from the full name of current class. All tags are available as options:
144
+
145
+ ```ruby
146
+ class Article::PublicationPolicy < Tram::Policy
147
+ # ...
148
+ errors.add :empty_text, field: "text", level: "error"
149
+ # ...
150
+ end
151
+ ```
152
+
153
+ ```yaml
154
+ # /config/locales/en.yml
155
+ ---
156
+ en:
157
+ article/publication_policy:
158
+ empty_text: "Validation %{level}: %{field} is empty"
159
+ ```
160
+
161
+ This will provide error message "Validation error: text is empty".
162
+
163
+ The last thing to say is about exceptions. When you use `validate!` it raises `Tram::Policy::ValidationError` (subclass of `RuntimeError`). Its message is built from selected errors (taking into account a `validation!` filter).
164
+
165
+ The exception also carries a backreference to the `policy` that raised it. You can use it to extract either errors, or arguments of the policy during a debugging:
166
+
167
+ ```ruby
168
+ begin
169
+ policy.validate!
170
+ rescue Tram::Policy::ValidationError => error
171
+ error.policy == policy # => true
172
+ end
173
+ ```
174
+
175
+ ## RSpec matchers
176
+
177
+ RSpec matchers defined in a file `tram-policy/matcher` (not loaded in runtime).
178
+
179
+ Use `be_invalid_at` matcher to check whether a policy has errors with given tags.
180
+
181
+ ```ruby
182
+ # app/policies/user/readiness_policy.rb
183
+ class User::ReadinessPolicy < Tram::Policy
184
+ option :name, proc(&:to_s), optional: true
185
+ option :email, proc(&:to_s), optional: true
186
+
187
+ validate :name_presence
188
+
189
+ private
190
+
191
+ def name_presence
192
+ return unless name.empty?
193
+ errors.add "Name is absent", level: "error"
194
+ end
195
+ end
196
+ ```
197
+
198
+ ```ruby
199
+ # spec/spec_helper.rb
200
+ require "tram-policy/matcher"
201
+ ```
202
+
203
+ ```ruby
204
+ # spec/policies/user/readiness_policy_spec.rb
205
+ RSpec.describe User::ReadinessPolicy do
206
+ let(:user) { build :user } # <- expected a factory
207
+
208
+ subject(:policy) { described_class[email: "user@example.com"] }
209
+
210
+ it "is invalid with 'error' level" do
211
+ expect { policy }.to be_invalid_at level: "error"
212
+ end
213
+
214
+ it "is not invalid with 'info' level" do
215
+ expect { policy }.not_to be_invalid_at level: "info"
216
+ end
217
+ end
218
+ ```
219
+
220
+ **Notice** that you have to wrap policy into block `{ policy }`. This is because matcher checks not only presence of an error, but also ensures its message is translated to all available locales (`I18n.available_locales`). The block containing a policy will be executed separately for every such language.
221
+
222
+ ## Generators
223
+
224
+ The gem provides simple tool for scaffolding new policy along with RSpec test template.
225
+
226
+ ```shell
227
+ $ tram-policy user/readiness_policy -p user -o admin -v name_present email_present
228
+ ```
229
+
230
+ This will generate a policy class with specification compatible to both [RSpec][rspec] and [FactoryGirl][factory-girl]:
231
+
232
+
233
+ ```ruby
234
+ # app/policies/user/readiness_policy.rb
235
+ class User::ReadinessPolicy < Tram::Policy
236
+ param :user
237
+ option :admin
238
+
239
+ validate :name_present
240
+ validate :email_present
241
+
242
+ private
243
+
244
+ def name_present
245
+ return if true # modify condition
246
+ errors.add :name_present # add necessary tags
247
+ end
248
+
249
+ def email_present
250
+ return if true # modify condition
251
+ errors.add :email_present # add necessary tags
252
+ end
253
+ end
254
+ ```
255
+
256
+ ```yaml
257
+ # config/tram-policies.en.yml
258
+ ---
259
+ en:
260
+ user/readiness_policy:
261
+ name_present: name_present
262
+ email_present: email_present
263
+ ```
264
+
265
+ ```ruby
266
+ # spec/policies/user/readiness_policy_spec.rb
267
+ RSpec.describe User::ReadinessPolicy do
268
+ let(:user) { build :user } # <- expected a factory
269
+
270
+ subject(:policy) { described_class[user] }
271
+
272
+ it { is_expected.to be_valid }
273
+
274
+ it "is invalid when not name_present" do
275
+ policy # modify it correspondingly
276
+ expect { policy }.to be_invalid_at # add tags to check
277
+ end
278
+
279
+ it "is invalid when not email_present" do
280
+ policy # modify it correspondingly
281
+ expect { policy }.to be_invalid_at # add tags to check
282
+ end
283
+ end
284
+ ```
285
+
286
+ Later you can copy-paste that contexts to provide more edge case for testing your policies.
287
+
288
+ Notice that RSpec matcher `be_invalid_at` checks at once:
289
+
290
+ - that an error is added to the policy
291
+ - that the error has given tags
292
+ - that the error is translated to every available locale
293
+
294
+ and provides full description for the essence of the failure.
295
+
296
+ ## To Recap
297
+
298
+ The `Tram::Policy` DSL provides the following methods:
299
+
300
+ * `.param` and `.option` - class-level methods for policy constructor arguments
301
+ * `.validate` - class-level method to add validators (they will be invoked in the same order as defined)
302
+ * `.[]` - a syntax sugar for `.new`
303
+
304
+ * `#errors` - returns an enumerable collection of validation errors
305
+ * `#valid?` - checks whether no errors exist
306
+ * `#invalid?` - checks whether some error exists
307
+ * `#validate!` - raises if some error exist
308
+
309
+ Enumerable collection of unique policy `errors` (`Tram::Policy::Errors`) responds to methods:
310
+
311
+ * `add` - adds an error to the collection
312
+ * `each` - iterates by the set of errors (support other methods of enumerables)
313
+ * `empty?` - checks whether a collection is emtpy (in addition to enumerable interface)
314
+ * `by_tags` - filters errors that have given tags
315
+ * `messages` - returns an array of messages
316
+ * `full_messages` - returns an array of messages with tags info added (used in exception)
317
+ * `merge` - merges a collection to another one
318
+
319
+ Every instance of `Tram::Policy::Error` supports:
320
+
321
+ * `#tags` - hash of assigned tags
322
+ * `#message` - the translated message
323
+ * `#full_message` - the message with tags info added
324
+ * `#to_h` - hash of tags and a message
325
+ * `#==` - checks whether an error is equal to another one
326
+ * undefined methods treated as tags
327
+
328
+ The instance of `Tram::Policy::ValidationError` responds to:
329
+
330
+ * `policy` - returns a policy object that raised an exception
331
+ * other methods defined by the `RuntimeError` class
332
+
333
+ ## Installation
334
+
335
+ Add this line to your application's Gemfile:
336
+
337
+ ```ruby
338
+ gem 'tram-policy'
339
+ ```
340
+
341
+ And then execute:
342
+
343
+ ```shell
344
+ $ bundle
345
+ ```
346
+
347
+ Or install it yourself as:
348
+
349
+ ```shell
350
+ $ gem install tram-policy
351
+ ```
352
+
353
+ ## License
354
+
355
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
356
+
357
+ [codeclimate-badger]: https://img.shields.io/codeclimate/github/tram-rb/tram-policy.svg?style=flat
358
+ [codeclimate]: https://codeclimate.com/github/tram-rb/tram-policy
359
+ [gem-badger]: https://img.shields.io/gem/v/tram-policy.svg?style=flat
360
+ [gem]: https://rubygems.org/gems/tram-policy
361
+ [gemnasium-badger]: https://img.shields.io/gemnasium/tram-rb/tram-policy.svg?style=flat
362
+ [gemnasium]: https://gemnasium.com/tram-rb/tram-policy
363
+ [inch-badger]: http://inch-ci.org/github/tram-rb/tram-policy.svg
364
+ [inch]: https://inch-ci.org/github/tram-rb/tram-policy
365
+ [travis-badger]: https://img.shields.io/travis/tram-rb/tram-policy/master.svg?style=flat
366
+ [travis]: https://travis-ci.org/tram-rb/tram-policy
367
+ [active-model-validation]: http://api.rubyonrails.org/classes/ActiveModel/Validations.html
368
+ [active-record-validation]: http://guides.rubyonrails.org/active_record_validations.html
369
+ [dry-validation]: http://dry-rb.org/gems/dry-validation/
370
+ [dry-initializer]: http://dry-rb.org/gems/dry-initializer/
371
+ [i18n]: https://github.com/svenfuchs/i18n
372
+ [rspec]: http://rspec.info/
373
+ [factory-girl]: https://github.com/thoughtbot/factory_girl