tram-policy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +373 -0
- data/Rakefile +7 -0
- data/bin/tram-policy +4 -0
- data/lib/tram-policy.rb +1 -0
- data/lib/tram/policy.rb +113 -0
- data/lib/tram/policy/error.rb +88 -0
- data/lib/tram/policy/errors.rb +102 -0
- data/lib/tram/policy/generator.rb +111 -0
- data/lib/tram/policy/generator/policy.erb +20 -0
- data/lib/tram/policy/generator/policy_spec.erb +17 -0
- data/lib/tram/policy/inflector.rb +26 -0
- data/lib/tram/policy/matchers.rb +112 -0
- data/lib/tram/policy/validation_error.rb +18 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/tram/policy/error_spec.rb +61 -0
- data/spec/tram/policy/errors_spec.rb +112 -0
- data/spec/tram/policy/inflector_spec.rb +14 -0
- data/spec/tram/policy/matchers_spec.rb +70 -0
- data/spec/tram/policy/validation_error_spec.rb +23 -0
- data/spec/tram/policy_spec.rb +173 -0
- data/tram-policy.gemspec +25 -0
- metadata +182 -0
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
data/.gitignore
ADDED
data/.rspec
ADDED
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
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
|