tram-policy 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +28 -1
- data/README.md +107 -83
- data/lib/tram/policy.rb +27 -2
- data/lib/tram/policy/dsl.rb +22 -1
- data/lib/tram/policy/error.rb +64 -21
- data/lib/tram/policy/errors.rb +46 -18
- data/lib/tram/policy/rspec.rb +5 -3
- data/lib/tram/policy/validation_error.rb +1 -1
- data/spec/fixtures/en.yml +2 -0
- data/spec/spec_helper.rb +7 -3
- data/spec/tram/policy/error_spec.rb +13 -26
- data/spec/tram/policy/errors_spec.rb +34 -40
- data/spec/tram/policy/rspec_spec.rb +1 -4
- data/spec/tram/policy/validation_error_spec.rb +2 -2
- data/tram-policy.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ffe8d541eb55eb7ba031f7578515523884d74a9
|
4
|
+
data.tar.gz: b0e194a4dcd4682c79526a024b60f574b32014ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9413e5c3d0bf479d3022ce75bbbcd639fee2ffa6632883859419b724ff7cb9b04c792b4118837f854714611b2db1756ca3294ead1767fc9ab669c8c7e0b8806c
|
7
|
+
data.tar.gz: 4be7c88f9c2e1bdb45c70c45c90367afe2b1a8616837ca121a07a769521c8e1c1e3bb29224678d2425a49473db4b6549f4d71b9d9ca0e3e8f2eb0d3715e19138
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
6
|
|
7
|
+
## [0.4.0] - [2018-02-17]
|
8
|
+
|
9
|
+
This is beta-release before the first stable version 1.0.0.
|
10
|
+
|
11
|
+
It adds methods `#item` and `#items` to policy errors to support lazy translation.
|
12
|
+
|
13
|
+
It also renames some methods, and deprecate others that will be removed from v1.0.0.
|
14
|
+
|
15
|
+
### Added
|
16
|
+
- `Tram::Policy.root_scope` changes the default root scope ("tram-policy") for I18n (nepalez)
|
17
|
+
- `Tram::Policy::Error#item` returns an array of [key, tags] which can be sent to I18n.t later (nepalez)
|
18
|
+
- `Tram::Policy::Error#to_a` as an alias for the `#item` (nepalez)
|
19
|
+
- `Tram::Policy::Errors#items` returns an array of error items (nepalez)
|
20
|
+
- `Tram::Policy::Errors#filter` acts like `by_tag` but returns the filtered collection instead of an array (nepalez)
|
21
|
+
- `Tram::Policy#messages` as a shortcut for `errors.messages` (nepalez)
|
22
|
+
- `Tram::Policy#items` as a shortcut for `errors.items` (nepalez)
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
- errors are compared by `#to_a` instead of `#to_h` (nepalez)
|
26
|
+
|
27
|
+
### Deprecated
|
28
|
+
- `Tram::Policy::Error#full_message` (nepalez)
|
29
|
+
- `Tram::Policy::Error#to_h` (nepalez)
|
30
|
+
- `Tram::Policy::Errors#full_messages` (nepalez)
|
31
|
+
- `Tram::Policy::Errors#by_tags` (nepalez)
|
32
|
+
|
7
33
|
## [0.3.1] - [2018-01-05]
|
8
34
|
|
9
35
|
### Fixed
|
@@ -114,7 +140,7 @@ The gem is battle-tested for production (in a real commertial project).
|
|
114
140
|
|
115
141
|
Use a multiline version instead of `validate :foo, :bar`:
|
116
142
|
|
117
|
-
```
|
143
|
+
```ruby
|
118
144
|
validate :foo
|
119
145
|
validate :bar
|
120
146
|
```
|
@@ -143,3 +169,4 @@ This is a first public release (@nepalez, @charlie-wasp, @JewelSam, @sergey-chec
|
|
143
169
|
[0.2.5]: https://github.com/tram-rb/tram-policy/compare/v0.2.4...v0.2.5
|
144
170
|
[0.3.0]: https://github.com/tram-rb/tram-policy/compare/v0.2.5...v0.3.0
|
145
171
|
[0.3.1]: https://github.com/tram-rb/tram-policy/compare/v0.3.0...v0.3.1
|
172
|
+
[0.4.0]: https://github.com/tram-rb/tram-policy/compare/v0.3.1...v0.4.0
|
data/README.md
CHANGED
@@ -14,12 +14,12 @@ Policy Object Pattern
|
|
14
14
|
|
15
15
|
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.
|
16
16
|
|
17
|
-
There are several well-known interfaces exist for validation like [ActiveModel::Validations][active-model-validation], or its [ActiveRecord][active-record-validation] extension
|
17
|
+
There are several well-known interfaces exist for validation like [ActiveModel::Validations][active-model-validation], or its [ActiveRecord][active-record-validation] extension for Rails, or PORO [Dry::Validation][dry-validation]. All of them focus on providing rich DSL-s for **validation rules**.
|
18
18
|
|
19
19
|
**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.
|
20
20
|
|
21
|
-
- By **customizable** we mean adding any number of *tags* to
|
22
|
-
- By **composable** we mean a possibility to merge errors provided by one policy
|
21
|
+
- By **customizable** we mean adding any number of *tags* to errors -- to allow filtering and sorting validation results.
|
22
|
+
- By **composable** we mean a possibility to merge errors provided by one policy into another, and build nested sets of well-focused policies.
|
23
23
|
|
24
24
|
Keeping this reasons in mind, let's go to some examples.
|
25
25
|
|
@@ -51,22 +51,24 @@ class Article::ReadinessPolicy < Tram::Policy
|
|
51
51
|
|
52
52
|
def title_presence
|
53
53
|
return unless title.empty?
|
54
|
-
# Adds an error with a
|
54
|
+
# Adds an error with a unique key and a set of additional tags
|
55
55
|
# You can use any tags, not only an attribute/field like in ActiveModel
|
56
|
-
errors.add
|
56
|
+
errors.add :blank_title, field: "title", level: "error"
|
57
57
|
end
|
58
58
|
|
59
59
|
def subtitle_presence
|
60
60
|
return unless subtitle.empty?
|
61
61
|
# Notice that we can set another level
|
62
|
-
errors.add
|
62
|
+
errors.add :blank_subtitle, field: "subtitle", level: "warning"
|
63
63
|
end
|
64
64
|
end
|
65
65
|
```
|
66
66
|
|
67
|
-
Because validation is the only responsibility of a policy, we don't need to call it explicitly.
|
67
|
+
Because validation is the only responsibility of a policy, we don't need to call it explicitly.
|
68
68
|
|
69
|
-
|
69
|
+
Policy initializer will perform all the checks immediately, memoizing the results into `errors` array. The methods `#valid?`, `#invalid?` and `#validate!` just check those `#errors`.
|
70
|
+
|
71
|
+
You should treat an instance immutable.
|
70
72
|
|
71
73
|
```ruby
|
72
74
|
article = Article.new title: "A wonderful article", subtitle: "", text: ""
|
@@ -78,35 +80,117 @@ policy.valid? # => false
|
|
78
80
|
policy.invalid? # => true
|
79
81
|
policy.validate! # raises Tram::Policy::ValidationError
|
80
82
|
|
81
|
-
#
|
83
|
+
# And errors
|
82
84
|
policy.errors.count # => 2 (no subtitle, no text)
|
83
85
|
policy.errors.filter { |error| error.tags[:level] == "error" }.count # => 1
|
84
86
|
policy.errors.filter { |error| error.level == "error" }.count # => 1
|
87
|
+
```
|
88
|
+
|
89
|
+
## Validation Results
|
90
|
+
|
91
|
+
Let look at those errors closer. We define 3 representation of errors:
|
92
|
+
|
93
|
+
- error objects (`policy.errors`)
|
94
|
+
- error items (`policy.items`, `policy.errors.items`, `policy.errors.map(&:item)`)
|
95
|
+
- error messages (`policy.messages`, `policy.errors.messages`, `policy.errors.map(&:message)`)
|
96
|
+
|
97
|
+
Errors by themselves are used for composition (see the next chapter), while `items` and `messages` represent errors for translation.
|
98
|
+
|
99
|
+
The difference is the following.
|
100
|
+
|
101
|
+
- The `messages` are translated immediately using the current locale.
|
102
|
+
|
103
|
+
- The `items` postpone translation for later (for example, you can store them in a database and translate them to the locale of UI by demand).
|
104
|
+
|
105
|
+
### Items
|
85
106
|
|
86
|
-
|
87
|
-
policy.errors.map(&:message) # => ["Subtitle is empty", "Error translation for missed text"]
|
107
|
+
Error items contain arrays that could be send to I18n.t for translation. We add the default scope from the name of policy, preceeded by the `["tram-policy"]` root namespace.
|
88
108
|
|
89
|
-
|
90
|
-
policy.
|
109
|
+
```ruby
|
110
|
+
policy.items # or policy.errors.items, or policy.errors.map(&:item)
|
111
|
+
# => [
|
112
|
+
# [
|
113
|
+
# :blank_title,
|
114
|
+
# {
|
115
|
+
# scope: ["tram-policy", "article/readiness_policy"]],
|
116
|
+
# field: "title",
|
117
|
+
# level: "error"
|
118
|
+
# }
|
119
|
+
# ],
|
120
|
+
# ...
|
121
|
+
# ]
|
122
|
+
|
123
|
+
I18n.t(*policy.items.first)
|
124
|
+
# => "translation missing: en.tram-policy.article/readiness_policy.blank_title"
|
125
|
+
```
|
126
|
+
|
127
|
+
You can change the root scope if you will (this could be useful in libraries):
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class MyGemPolicy < Tram::Policy
|
131
|
+
scope "mygem", "policies" # inherited by subclasses
|
132
|
+
end
|
133
|
+
|
134
|
+
class Article::ReadinessPolicy < MyGemPolicy
|
135
|
+
# ...
|
136
|
+
end
|
137
|
+
|
138
|
+
# ...
|
139
|
+
I18n.t(*policy.items.first)
|
140
|
+
# => "translation missing: en.mygem.policies.article/readiness_policy.blank_title"
|
141
|
+
```
|
142
|
+
|
143
|
+
### Messages
|
144
|
+
|
145
|
+
Error messages contain translation of `policy.items` in the current locale:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
policy.messages # or policy.errors.messages, or policy.errors.map(&:message)
|
149
|
+
# => [
|
150
|
+
# "translation missing: en.tram-policy.article/readiness_policy.blank_title",
|
151
|
+
# "translation missing: en.tram-policy.article/readiness_policy.blank_subtitle"
|
152
|
+
# ]
|
153
|
+
```
|
154
|
+
|
155
|
+
The messages are translated if the keys are symbolic. Strings are treated as already translated:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class Article::ReadinessPolicy < Tram::Policy
|
159
|
+
# ...
|
160
|
+
def title_presence
|
161
|
+
return unless title.empty?
|
162
|
+
errors.add "Title is absent", field: "title", level: "error"
|
163
|
+
end
|
164
|
+
end
|
91
165
|
|
92
|
-
#
|
93
|
-
policy.
|
166
|
+
# ...
|
167
|
+
policy.messages
|
94
168
|
# => [
|
95
|
-
#
|
96
|
-
#
|
169
|
+
# "Title is absent",
|
170
|
+
# "translation missing: en.tram-policy.article/readiness_policy.blank_subtitle"
|
97
171
|
# ]
|
172
|
+
```
|
173
|
+
|
174
|
+
## Partial Validation
|
98
175
|
|
99
|
-
|
176
|
+
You can use tags in checkers -- to add condition for errors to ignore
|
177
|
+
|
178
|
+
```ruby
|
100
179
|
policy.valid? { |error| !%w(warning error).include? error.level } # => false
|
101
180
|
policy.valid? { |error| error.level != "disaster" } # => true
|
181
|
+
```
|
102
182
|
|
103
|
-
|
183
|
+
Notice the `invalid?` method takes a block with definitions for errors to count (not ignore)
|
184
|
+
|
185
|
+
```ruby
|
104
186
|
policy.invalid? { |error| %w(warning error).include? error.level } # => true
|
105
187
|
policy.invalid? { |error| error.level == "disaster" } # => false
|
106
188
|
|
107
189
|
policy.validate! { |error| error.level != "disaster" } # => nil (seems ok)
|
108
190
|
```
|
109
191
|
|
192
|
+
## Composition of Policies
|
193
|
+
|
110
194
|
You can use errors in composition of policies:
|
111
195
|
|
112
196
|
```ruby
|
@@ -121,7 +205,7 @@ class Article::PublicationPolicy < Tram::Policy
|
|
121
205
|
|
122
206
|
def article_readiness
|
123
207
|
# Collects errors tagged by level: "error" from "nested" policy
|
124
|
-
readiness_errors = Article::ReadinessPolicy[article].errors.
|
208
|
+
readiness_errors = Article::ReadinessPolicy[article].errors.filter(level: "error")
|
125
209
|
|
126
210
|
# Merges collected errors to the current ones.
|
127
211
|
# New errors are also tagged by source: "readiness".
|
@@ -134,32 +218,9 @@ class Article::PublicationPolicy < Tram::Policy
|
|
134
218
|
end
|
135
219
|
```
|
136
220
|
|
137
|
-
|
138
|
-
|
139
|
-
> You can redefine the scope by reloading private method `.scope` of the policy.
|
140
|
-
|
141
|
-
All tags are available as options:
|
221
|
+
## Exceptions
|
142
222
|
|
143
|
-
|
144
|
-
class Article::PublicationPolicy < Tram::Policy
|
145
|
-
# ...
|
146
|
-
errors.add :empty, field: "text", level: "error"
|
147
|
-
# ...
|
148
|
-
end
|
149
|
-
```
|
150
|
-
|
151
|
-
```yaml
|
152
|
-
# /config/locales/en.yml
|
153
|
-
---
|
154
|
-
en:
|
155
|
-
tram-policy:
|
156
|
-
article/publication_policy:
|
157
|
-
empty: "Validation %{level}: %{field} is empty"
|
158
|
-
```
|
159
|
-
|
160
|
-
This will provide error message "Validation error: text is empty".
|
161
|
-
|
162
|
-
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).
|
223
|
+
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).
|
163
224
|
|
164
225
|
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:
|
165
226
|
|
@@ -238,7 +299,7 @@ RSpec.describe User::ReadinessPolicy do
|
|
238
299
|
end
|
239
300
|
```
|
240
301
|
|
241
|
-
**Notice** that you have to wrap policy into block `{ policy }`. This is because the 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.
|
302
|
+
**Notice** that you have to wrap policy into block `{ policy }`. This is because the matcher checks not only the 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.
|
242
303
|
|
243
304
|
## Generators
|
244
305
|
|
@@ -342,43 +403,6 @@ When called without tags, it checks that the policy is valid as a whole.
|
|
342
403
|
|
343
404
|
Both matchers provide a full description for the essence of the failure.
|
344
405
|
|
345
|
-
## To Recap
|
346
|
-
|
347
|
-
The `Tram::Policy` DSL provides the following methods:
|
348
|
-
|
349
|
-
* `.param` and `.option` - class-level methods for policy constructor arguments
|
350
|
-
* `.validate` - class-level method to add validators (they will be invoked in the same order as defined)
|
351
|
-
* `.[]` - a syntax sugar for `.new`
|
352
|
-
|
353
|
-
* `#errors` - returns an enumerable collection of validation errors
|
354
|
-
* `#valid?` - checks whether no errors exist
|
355
|
-
* `#invalid?` - checks whether some error exists
|
356
|
-
* `#validate!` - raises if some error exist
|
357
|
-
|
358
|
-
Enumerable collection of unique policy `errors` (`Tram::Policy::Errors`) responds to methods:
|
359
|
-
|
360
|
-
* `add` - adds an error to the collection
|
361
|
-
* `each` - iterates by the set of errors (support other methods of enumerables)
|
362
|
-
* `empty?` - checks whether a collection is emtpy (in addition to enumerable interface)
|
363
|
-
* `by_tags` - filters errors that have given tags
|
364
|
-
* `messages` - returns an array of messages
|
365
|
-
* `full_messages` - returns an array of messages with tags info added (used in exception)
|
366
|
-
* `merge` - merges a collection to another one
|
367
|
-
|
368
|
-
Every instance of `Tram::Policy::Error` supports:
|
369
|
-
|
370
|
-
* `#tags` - hash of assigned tags
|
371
|
-
* `#message` - the translated message
|
372
|
-
* `#full_message` - the message with tags info added
|
373
|
-
* `#to_h` - hash of tags and a message
|
374
|
-
* `#==` - checks whether an error is equal to another one
|
375
|
-
* undefined methods treated as tags
|
376
|
-
|
377
|
-
The instance of `Tram::Policy::ValidationError` responds to:
|
378
|
-
|
379
|
-
* `policy` - returns a policy object that raised an exception
|
380
|
-
* other methods defined by the `RuntimeError` class
|
381
|
-
|
382
406
|
## Installation
|
383
407
|
|
384
408
|
Add this line to your application's Gemfile:
|
data/lib/tram/policy.rb
CHANGED
@@ -15,6 +15,15 @@ module Tram
|
|
15
15
|
extend Dry::Initializer
|
16
16
|
extend DSL
|
17
17
|
|
18
|
+
# The scope used for translating error messages
|
19
|
+
#
|
20
|
+
# @return [Array<String>]
|
21
|
+
#
|
22
|
+
def scope
|
23
|
+
Array self.class.scope
|
24
|
+
end
|
25
|
+
|
26
|
+
# @!method t(message, options)
|
18
27
|
# Translates a message in the scope of current policy
|
19
28
|
#
|
20
29
|
# @param [#to_s] message
|
@@ -23,10 +32,10 @@ module Tram
|
|
23
32
|
#
|
24
33
|
def t(message, **options)
|
25
34
|
return message.to_s unless message.is_a? Symbol
|
26
|
-
I18n.t message, scope:
|
35
|
+
I18n.t message, scope: scope, **options
|
27
36
|
end
|
28
37
|
|
29
|
-
#
|
38
|
+
# The collection of validation errors
|
30
39
|
#
|
31
40
|
# @return [Tram::Policy::Errors]
|
32
41
|
#
|
@@ -34,6 +43,22 @@ module Tram
|
|
34
43
|
@errors ||= Errors.new(self)
|
35
44
|
end
|
36
45
|
|
46
|
+
# The array of error items for lazy translation
|
47
|
+
#
|
48
|
+
# @return [Array<Array>]
|
49
|
+
#
|
50
|
+
def items
|
51
|
+
errors.items
|
52
|
+
end
|
53
|
+
|
54
|
+
# The array of error messages translated for the current locale
|
55
|
+
#
|
56
|
+
# @return [Array<String>]
|
57
|
+
#
|
58
|
+
def messages
|
59
|
+
errors.messages
|
60
|
+
end
|
61
|
+
|
37
62
|
# Checks whether the policy is valid
|
38
63
|
#
|
39
64
|
# @param [Proc, nil] filter Block describing **errors to be skipped**
|
data/lib/tram/policy/dsl.rb
CHANGED
@@ -22,12 +22,21 @@ class Tram::Policy
|
|
22
22
|
new(*args)
|
23
23
|
end
|
24
24
|
|
25
|
+
# Sets the root scope of the policy and its subclasses
|
26
|
+
#
|
27
|
+
# @param [String, Array<String>] value
|
28
|
+
# @return [self]
|
29
|
+
#
|
30
|
+
def root_scope(*value)
|
31
|
+
tap { @root_scope = value.flatten.map(&:to_s).reject(&:empty?) }
|
32
|
+
end
|
33
|
+
|
25
34
|
# Translation scope for a policy
|
26
35
|
#
|
27
36
|
# @return [Array<String>]
|
28
37
|
#
|
29
38
|
def scope
|
30
|
-
@scope ||= [
|
39
|
+
@scope ||= Array(@root_scope) + [Inflector.underscore(name)]
|
31
40
|
end
|
32
41
|
|
33
42
|
# List of validators defined by a policy per se
|
@@ -46,5 +55,17 @@ class Tram::Policy
|
|
46
55
|
parent_validators = self == Tram::Policy ? [] : superclass.validators
|
47
56
|
(parent_validators + local_validators).uniq
|
48
57
|
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def inherited(klass)
|
62
|
+
super
|
63
|
+
klass.send :instance_variable_set, :@root_scope, @root_scope
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.extended(klass)
|
67
|
+
super
|
68
|
+
klass.send :instance_variable_set, :@root_scope, %w[tram-policy]
|
69
|
+
end
|
49
70
|
end
|
50
71
|
end
|
data/lib/tram/policy/error.rb
CHANGED
@@ -6,7 +6,7 @@ class Tram::Policy
|
|
6
6
|
# from one collection of [Tram::Policy::Errors] to another.
|
7
7
|
#
|
8
8
|
class Error
|
9
|
-
# @!method self.new(value, opts)
|
9
|
+
# @!method self.new(value, opts = {})
|
10
10
|
# Builds an error
|
11
11
|
#
|
12
12
|
# If another error is send to the constructor, the error returned unchanged
|
@@ -16,65 +16,108 @@ class Tram::Policy
|
|
16
16
|
# @return [Tram::Policy::Error]
|
17
17
|
#
|
18
18
|
def self.new(value, **opts)
|
19
|
-
|
20
|
-
super
|
19
|
+
value.instance_of?(self) ? value : super
|
21
20
|
end
|
22
21
|
|
23
|
-
# @!attribute [r]
|
22
|
+
# @!attribute [r] key
|
23
|
+
# @return [Symbol, String] error key
|
24
|
+
attr_reader :key
|
25
|
+
|
26
|
+
# @!attribute [r] tags
|
27
|
+
# @return [Hash<Symbol, Object>] error tags
|
28
|
+
attr_reader :tags
|
29
|
+
|
30
|
+
# The list of arguments for [I18n.t]
|
24
31
|
#
|
25
|
-
# @return [
|
32
|
+
# @return [Array]
|
26
33
|
#
|
27
|
-
|
34
|
+
def item
|
35
|
+
[key, tags]
|
36
|
+
end
|
37
|
+
alias to_a item
|
28
38
|
|
29
|
-
# The
|
39
|
+
# The text of error message translated to the current locale
|
30
40
|
#
|
31
41
|
# @return [String]
|
32
42
|
#
|
33
|
-
def
|
34
|
-
|
43
|
+
def message
|
44
|
+
key.is_a?(Symbol) ? I18n.t(*item) : key.to_s
|
35
45
|
end
|
36
46
|
|
37
|
-
#
|
47
|
+
# @deprecated
|
48
|
+
# Converts the error to a hash of message and tags
|
38
49
|
#
|
39
50
|
# @return [Hash<Symbol, Object>]
|
40
51
|
#
|
41
52
|
def to_h
|
42
|
-
|
53
|
+
warn "[DEPRECATED] The method Tram::Policy::Error#to_h" \
|
54
|
+
" will be removed in the v1.0.0.."
|
55
|
+
|
56
|
+
tags.reject { |k| k == :scope }.merge(message: message)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @deprecated
|
60
|
+
# The full message (message and tags info)
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
#
|
64
|
+
def full_message
|
65
|
+
warn "[DEPRECATED] The method Tram::Policy::Error#full_message" \
|
66
|
+
" will be removed in the v1.0.0."
|
67
|
+
|
68
|
+
[message, tags].reject(&:empty?).join(" ")
|
43
69
|
end
|
44
70
|
|
45
|
-
# Fetches
|
71
|
+
# Fetches an option
|
46
72
|
#
|
47
73
|
# @param [#to_sym] tag
|
48
74
|
# @return [Object]
|
49
75
|
#
|
50
76
|
def [](tag)
|
51
|
-
|
77
|
+
tags[tag.to_sym]
|
52
78
|
end
|
53
79
|
|
54
|
-
# Fetches
|
80
|
+
# Fetches the tag
|
55
81
|
#
|
56
82
|
# @param [#to_sym] tag
|
57
83
|
# @param [Object] default
|
58
84
|
# @param [Proc] block
|
59
85
|
# @return [Object]
|
60
86
|
#
|
61
|
-
def fetch(tag, default, &block)
|
62
|
-
|
87
|
+
def fetch(tag, default = Dry::Initializer::UNDEFINED, &block)
|
88
|
+
if default == Dry::Initializer::UNDEFINED
|
89
|
+
tags.fetch(tag.to_sym, &block)
|
90
|
+
else
|
91
|
+
tags.fetch(tag.to_sym, default, &block)
|
92
|
+
end
|
63
93
|
end
|
64
94
|
|
65
|
-
# Compares an error to another object using method [#
|
95
|
+
# Compares an error to another object using method [#item]
|
66
96
|
#
|
67
97
|
# @param [Object] other Other object to compare to
|
68
98
|
# @return [Boolean]
|
69
99
|
#
|
70
100
|
def ==(other)
|
71
|
-
other.respond_to?(:
|
101
|
+
other.respond_to?(:to_a) && other.to_a == item
|
102
|
+
end
|
103
|
+
|
104
|
+
# @!method contain?(some_key = nil, some_tags = {})
|
105
|
+
# Checks whether the error contain given key and tags
|
106
|
+
#
|
107
|
+
# @param [Object] some_key Expected key of the error
|
108
|
+
# @param [Hash<Symbol, Object>] some_tags Expected tags of the error
|
109
|
+
# @return [Boolean]
|
110
|
+
#
|
111
|
+
def contain?(some_key = nil, **some_tags)
|
112
|
+
return false if some_key&.!= key
|
113
|
+
some_tags.each { |k, v| return false unless tags[k] == v }
|
114
|
+
true
|
72
115
|
end
|
73
116
|
|
74
117
|
private
|
75
118
|
|
76
|
-
def initialize(
|
77
|
-
@
|
119
|
+
def initialize(key, **tags)
|
120
|
+
@key = key
|
78
121
|
@tags = tags
|
79
122
|
end
|
80
123
|
|
@@ -83,7 +126,7 @@ class Tram::Policy
|
|
83
126
|
end
|
84
127
|
|
85
128
|
def method_missing(name, *args, &block)
|
86
|
-
args.any? || block ? super :
|
129
|
+
args.any? || block ? super : tags[name]
|
87
130
|
end
|
88
131
|
end
|
89
132
|
end
|
data/lib/tram/policy/errors.rb
CHANGED
@@ -21,12 +21,10 @@ class Tram::Policy
|
|
21
21
|
# @param [Hash<Symbol, Object>] tags Tags to be attached to the message
|
22
22
|
# @return [self] the collection
|
23
23
|
#
|
24
|
-
def add(message
|
25
|
-
|
24
|
+
def add(message, **tags)
|
25
|
+
tags = tags.merge(scope: policy.scope) unless tags.key?(:scope)
|
26
26
|
raise ArgumentError.new("Error message should be defined") unless message
|
27
|
-
|
28
|
-
@set << Tram::Policy::Error.new(@policy.t(message, tags), **tags)
|
29
|
-
self
|
27
|
+
tap { @set << Tram::Policy::Error.new(message, **tags) }
|
30
28
|
end
|
31
29
|
|
32
30
|
# Iterates by collected errors
|
@@ -38,15 +36,32 @@ class Tram::Policy
|
|
38
36
|
@set.each { |error| yield(error) }
|
39
37
|
end
|
40
38
|
|
41
|
-
# @!method
|
42
|
-
#
|
39
|
+
# @!method filter(key = nil, tags)
|
40
|
+
# Filter errors by optional key and tags
|
43
41
|
#
|
44
|
-
# @param [
|
45
|
-
# @
|
42
|
+
# @param [#to_s] key The key to filter errors by
|
43
|
+
# @param [Hash<Symbol, Object>] tags The list of tags to filter errors by
|
44
|
+
# @return [Tram::Policy::Errors]
|
46
45
|
#
|
47
|
-
def
|
48
|
-
|
49
|
-
|
46
|
+
def filter(key = nil, **tags)
|
47
|
+
list = each_with_object(Set.new) do |error, obj|
|
48
|
+
obj << error if error.contain?(key, tags)
|
49
|
+
end
|
50
|
+
self.class.new(policy, list)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @deprecated
|
54
|
+
# @!method by_tags(tags)
|
55
|
+
# Selects errors filtered by key and tags
|
56
|
+
#
|
57
|
+
# @param [Hash<Symbol, Object>] tags List of options to filter by
|
58
|
+
# @return [Array<Tram::Policy::Error>]
|
59
|
+
#
|
60
|
+
def by_tags(**tags)
|
61
|
+
warn "[DEPRECATED] The method Tram::Policy::Errors#by_tags" \
|
62
|
+
" will be removed in the v1.0.0. Use method #filter instead."
|
63
|
+
|
64
|
+
filter(tags).to_a
|
50
65
|
end
|
51
66
|
|
52
67
|
# @!method empty?
|
@@ -58,6 +73,14 @@ class Tram::Policy
|
|
58
73
|
block ? !any?(&block) : !any?
|
59
74
|
end
|
60
75
|
|
76
|
+
# The array of error items for translation
|
77
|
+
#
|
78
|
+
# @return [Array<Array>]
|
79
|
+
#
|
80
|
+
def items
|
81
|
+
@set.map(&:item)
|
82
|
+
end
|
83
|
+
|
61
84
|
# The array of ordered error messages
|
62
85
|
#
|
63
86
|
# @return [Array<String>]
|
@@ -66,12 +89,16 @@ class Tram::Policy
|
|
66
89
|
@set.map(&:message).sort
|
67
90
|
end
|
68
91
|
|
69
|
-
#
|
92
|
+
# @deprecated
|
93
|
+
# List of error descriptions
|
70
94
|
#
|
71
95
|
# @return [Array<String>]
|
72
96
|
#
|
73
97
|
def full_messages
|
74
|
-
|
98
|
+
warn "[DEPRECATED] The method Tram::Policy::Errors#full_messages" \
|
99
|
+
" will be removed in the v1.0.0."
|
100
|
+
|
101
|
+
map(&:full_message)
|
75
102
|
end
|
76
103
|
|
77
104
|
# @!method merge(other, options)
|
@@ -90,8 +117,9 @@ class Tram::Policy
|
|
90
117
|
return self unless other.is_a?(self.class)
|
91
118
|
|
92
119
|
other.each do |err|
|
93
|
-
|
94
|
-
|
120
|
+
key, opts = err.item
|
121
|
+
opts = yield(opts) if block_given?
|
122
|
+
add key, opts.merge(options)
|
95
123
|
end
|
96
124
|
|
97
125
|
self
|
@@ -99,9 +127,9 @@ class Tram::Policy
|
|
99
127
|
|
100
128
|
private
|
101
129
|
|
102
|
-
def initialize(policy, errors =
|
130
|
+
def initialize(policy, errors = [])
|
103
131
|
@policy = policy
|
104
|
-
@set = errors
|
132
|
+
@set = Set.new(errors)
|
105
133
|
end
|
106
134
|
end
|
107
135
|
end
|
data/lib/tram/policy/rspec.rb
CHANGED
@@ -34,7 +34,9 @@ RSpec::Matchers.define :be_invalid_at do |**tags|
|
|
34
34
|
I18n.locale = locale
|
35
35
|
local_policy = policy_block.call
|
36
36
|
self.policy = local_policy.inspect
|
37
|
-
errors[locale] = local_policy&.errors&.
|
37
|
+
errors[locale] = local_policy&.errors&.filter(tags)&.map do |error|
|
38
|
+
{ message: error.message, tags: error.options } # translate immediately
|
39
|
+
end
|
38
40
|
end
|
39
41
|
|
40
42
|
def prepare_results(policy_block, tags)
|
@@ -62,7 +64,7 @@ RSpec::Matchers.define :be_invalid_at do |**tags|
|
|
62
64
|
|
63
65
|
# Checks if all collected errors are translated
|
64
66
|
def translated?
|
65
|
-
texts = errors.values.flatten.map
|
67
|
+
texts = errors.values.flatten.map { |err| err[:message] }
|
66
68
|
texts.select { |text| text.start_with?("translation missing") }.empty?
|
67
69
|
end
|
68
70
|
|
@@ -70,7 +72,7 @@ RSpec::Matchers.define :be_invalid_at do |**tags|
|
|
70
72
|
text = "Actual errors:\n"
|
71
73
|
errors.each do |locale, local_errors|
|
72
74
|
text << " #{locale}:\n"
|
73
|
-
local_errors&.each { |
|
75
|
+
local_errors&.each { |err| text << " - #{err.values.join(" ")}\n" }
|
74
76
|
end
|
75
77
|
text
|
76
78
|
end
|
@@ -11,7 +11,7 @@ class Tram::Policy
|
|
11
11
|
|
12
12
|
def initialize(policy, filter)
|
13
13
|
@policy = policy
|
14
|
-
messages = policy.errors.reject(&filter).map(&:
|
14
|
+
messages = policy.errors.to_a.reject(&filter).map(&:message)
|
15
15
|
super (["Validation failed with errors:"] + messages).join("\n- ")
|
16
16
|
end
|
17
17
|
end
|
data/spec/fixtures/en.yml
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -15,7 +15,11 @@ RSpec.configure do |config|
|
|
15
15
|
config.filter_run focus: true
|
16
16
|
config.run_all_when_everything_filtered = true
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
config.before(:each) do
|
19
|
+
Test = Class.new(Module)
|
20
|
+
I18n.available_locales = %w[en]
|
21
|
+
I18n.backend.store_translations :en, yaml_fixture_file("en.yml")["en"]
|
22
|
+
end
|
23
|
+
|
24
|
+
config.after(:each) { Object.send :remove_const, :Test }
|
21
25
|
end
|
@@ -1,45 +1,32 @@
|
|
1
1
|
RSpec.describe Tram::Policy::Error do
|
2
|
-
subject(:error) { described_class.new
|
2
|
+
subject(:error) { described_class.new :bad, options }
|
3
3
|
|
4
|
-
let(:
|
4
|
+
let(:options) { { level: "warning", scope: %w[tram-policy] } }
|
5
5
|
|
6
|
-
describe "#
|
7
|
-
subject { error.
|
8
|
-
it { is_expected.to eq
|
9
|
-
end
|
10
|
-
|
11
|
-
describe "#full_message" do
|
12
|
-
subject { error.full_message }
|
13
|
-
|
14
|
-
context "with tags:" do
|
15
|
-
it { is_expected.to eq "Something bad happened {:level=>\"warning\"}" }
|
16
|
-
end
|
17
|
-
|
18
|
-
context "without tags:" do
|
19
|
-
let(:tags) { {} }
|
20
|
-
it { is_expected.to eq "Something bad happened" }
|
21
|
-
end
|
6
|
+
describe "#item" do
|
7
|
+
subject { error.item }
|
8
|
+
it { is_expected.to eq [:bad, level: "warning", scope: %w[tram-policy]] }
|
22
9
|
end
|
23
10
|
|
24
|
-
describe "#
|
25
|
-
subject { error.
|
26
|
-
it { is_expected.to eq
|
11
|
+
describe "#message" do
|
12
|
+
subject { error.message }
|
13
|
+
it { is_expected.to eq "Something bad has happened" }
|
27
14
|
end
|
28
15
|
|
29
16
|
describe "#==" do
|
30
17
|
subject { error == other }
|
31
18
|
|
32
|
-
context "when other object has the same #
|
33
|
-
let(:other) { double
|
19
|
+
context "when other object has the same #item:" do
|
20
|
+
let(:other) { double to_a: error.item }
|
34
21
|
it { is_expected.to eq true }
|
35
22
|
end
|
36
23
|
|
37
|
-
context "when other object has different #
|
38
|
-
let(:other) { double
|
24
|
+
context "when other object has different #item:" do
|
25
|
+
let(:other) { double to_a: [:foo] }
|
39
26
|
it { is_expected.to eq false }
|
40
27
|
end
|
41
28
|
|
42
|
-
context "when other object not respond to #
|
29
|
+
context "when other object not respond to #item:" do
|
43
30
|
let(:other) { double }
|
44
31
|
it { is_expected.to eq false }
|
45
32
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
RSpec.describe Tram::Policy::Errors do
|
2
|
-
let(:policy) { double :policy,
|
2
|
+
let(:policy) { double :policy, scope: %w[tram-policy] }
|
3
3
|
let(:errors) { described_class.new(policy) }
|
4
4
|
|
5
5
|
describe ".new" do
|
@@ -12,14 +12,16 @@ RSpec.describe Tram::Policy::Errors do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
describe "#add" do
|
15
|
-
subject
|
15
|
+
subject { errors.add :omg, level: "info", field: "name" }
|
16
|
+
|
16
17
|
let(:error) { errors.to_a.last }
|
17
18
|
|
18
19
|
it "adds an error to the collection:" do
|
19
20
|
expect { 2.times { subject } }.to change { errors.count }.by 1
|
20
21
|
|
21
22
|
expect(error).to be_kind_of Tram::Policy::Error
|
22
|
-
expect(error)
|
23
|
+
expect(error)
|
24
|
+
.to eq [:omg, level: "info", field: "name", scope: %w[tram-policy]]
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
@@ -35,6 +37,13 @@ RSpec.describe Tram::Policy::Errors do
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
40
|
+
describe "#items" do
|
41
|
+
subject { errors.items }
|
42
|
+
|
43
|
+
before { errors.add "OMG!", level: "info", field: "name" }
|
44
|
+
it { is_expected.to eq errors.map(&:item) }
|
45
|
+
end
|
46
|
+
|
38
47
|
describe "#merge" do
|
39
48
|
let(:other) { described_class.new(policy) }
|
40
49
|
|
@@ -48,9 +57,9 @@ RSpec.describe Tram::Policy::Errors do
|
|
48
57
|
|
49
58
|
it "merges other collection as is" do
|
50
59
|
expect(subject).to be_a Tram::Policy::Errors
|
51
|
-
expect(subject.
|
52
|
-
|
53
|
-
|
60
|
+
expect(subject.items).to match_array [
|
61
|
+
["D'OH!", level: "disaster", scope: %w[tram-policy]],
|
62
|
+
["OUCH!", level: "error", scope: %w[tram-policy]]
|
54
63
|
]
|
55
64
|
end
|
56
65
|
end
|
@@ -60,9 +69,9 @@ RSpec.describe Tram::Policy::Errors do
|
|
60
69
|
|
61
70
|
it "merges filtered collection as is" do
|
62
71
|
expect(subject).to be_a Tram::Policy::Errors
|
63
|
-
expect(subject.
|
64
|
-
|
65
|
-
|
72
|
+
expect(subject.items).to match_array [
|
73
|
+
["D'OH!", level: "disaster", scope: %w[tram-policy]],
|
74
|
+
["OUCH!", level: "error", scope: %w[tram-policy], source: "Homer"]
|
66
75
|
]
|
67
76
|
end
|
68
77
|
end
|
@@ -72,9 +81,9 @@ RSpec.describe Tram::Policy::Errors do
|
|
72
81
|
|
73
82
|
it "merges other collection with given options" do
|
74
83
|
expect(subject).to be_a Tram::Policy::Errors
|
75
|
-
expect(subject.
|
76
|
-
|
77
|
-
|
84
|
+
expect(subject.items).to match_array [
|
85
|
+
["D'OH!", level: "disaster", scope: %w[tram-policy]],
|
86
|
+
["OUCH!", level: "error", scope: %w[tram-policy], source: "Homer"]
|
78
87
|
]
|
79
88
|
end
|
80
89
|
end
|
@@ -84,14 +93,14 @@ RSpec.describe Tram::Policy::Errors do
|
|
84
93
|
|
85
94
|
it "merges filtered collection with given options" do
|
86
95
|
expect(subject).to be_a Tram::Policy::Errors
|
87
|
-
expect(subject.
|
88
|
-
|
89
|
-
|
96
|
+
expect(subject.items).to match_array [
|
97
|
+
["D'OH!", level: "disaster", scope: %w[tram-policy]],
|
98
|
+
["OUCH!", level: "error", scope: %w[tram-policy], id: 5, age: 4]
|
90
99
|
]
|
91
100
|
end
|
92
101
|
end
|
93
102
|
|
94
|
-
context "
|
103
|
+
context "with no errors:" do
|
95
104
|
subject { errors.merge 1 }
|
96
105
|
it { is_expected.to eql errors }
|
97
106
|
end
|
@@ -100,26 +109,11 @@ RSpec.describe Tram::Policy::Errors do
|
|
100
109
|
describe "#messages" do
|
101
110
|
subject { errors.messages }
|
102
111
|
|
103
|
-
|
104
|
-
|
105
|
-
context "with errors added:" do
|
106
|
-
before { errors.add "OMG!", level: "info", field: "name" }
|
107
|
-
it { is_expected.to eq %w[OMG!] }
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
describe "#full_messages" do
|
112
|
-
subject { errors.full_messages }
|
113
|
-
|
114
|
-
it { is_expected.to eq [] }
|
115
|
-
|
116
|
-
context "with errors added:" do
|
117
|
-
before { errors.add "OMG!", level: "info", field: "name" }
|
118
|
-
it { is_expected.to eq ["OMG! {:level=>\"info\", :field=>\"name\"}"] }
|
119
|
-
end
|
112
|
+
before { errors.add "OMG!", level: "info", field: "name" }
|
113
|
+
it { is_expected.to eq errors.map(&:message) }
|
120
114
|
end
|
121
115
|
|
122
|
-
describe "#
|
116
|
+
describe "#filter" do
|
123
117
|
before do
|
124
118
|
errors.add :foo, field: "name", level: "error"
|
125
119
|
errors.add :foo, field: "email", level: "info"
|
@@ -127,21 +121,21 @@ RSpec.describe Tram::Policy::Errors do
|
|
127
121
|
end
|
128
122
|
|
129
123
|
context "with filter" do
|
130
|
-
subject { errors.
|
124
|
+
subject { errors.filter level: "error" }
|
131
125
|
|
132
126
|
it "returns selected errors only" do
|
133
|
-
expect(subject
|
134
|
-
|
135
|
-
|
127
|
+
expect(subject).to match_array [
|
128
|
+
[:foo, field: "name", level: "error", scope: %w[tram-policy]],
|
129
|
+
[:foo, field: "email", level: "error", scope: %w[tram-policy]]
|
136
130
|
]
|
137
131
|
end
|
138
132
|
end
|
139
133
|
|
140
134
|
context "without a filter" do
|
141
|
-
subject { errors.
|
135
|
+
subject { errors.filter }
|
142
136
|
|
143
137
|
it "returns selected all errors" do
|
144
|
-
expect(subject
|
138
|
+
expect(subject).to match_array errors.to_a
|
145
139
|
end
|
146
140
|
end
|
147
141
|
end
|
@@ -57,9 +57,6 @@ RSpec.describe "RSpec support:" do
|
|
57
57
|
|
58
58
|
describe "shared examples" do
|
59
59
|
it_behaves_like :invalid_policy
|
60
|
-
it_behaves_like :
|
61
|
-
before { I18n.available_locales = %i[en ru] }
|
62
|
-
end
|
63
|
-
it_behaves_like :valid_policy, field: "email"
|
60
|
+
it_behaves_like :valid_policy, field: "email"
|
64
61
|
end
|
65
62
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
RSpec.describe Tram::Policy::ValidationError do
|
2
2
|
subject(:error) { described_class.new policy, filter }
|
3
3
|
|
4
|
-
let(:one) { double
|
5
|
-
let(:two) { double
|
4
|
+
let(:one) { double message: "OMG!", level: "error" }
|
5
|
+
let(:two) { double message: "phew!", level: "warning" }
|
6
6
|
let(:policy) { double :policy, errors: [one, two] }
|
7
7
|
|
8
8
|
shared_examples :exception_with_messages do |text|
|
data/tram-policy.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = "tram-policy"
|
3
|
-
gem.version = "0.
|
3
|
+
gem.version = "0.4.0"
|
4
4
|
gem.author = ["Viktor Sokolov (gzigzigzeo)", "Andrew Kozin (nepalez)"]
|
5
5
|
gem.email = "andrew.kozin@gmail.com"
|
6
6
|
gem.homepage = "https://github.com/tram/tram-policy"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tram-policy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Viktor Sokolov (gzigzigzeo)
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-
|
12
|
+
date: 2018-02-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dry-initializer
|