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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c972ade529b23a0221126e7d486259729314c26
4
- data.tar.gz: 9f39b73542f528d32fc38db56ddf6122b8abae50
3
+ metadata.gz: 7ffe8d541eb55eb7ba031f7578515523884d74a9
4
+ data.tar.gz: b0e194a4dcd4682c79526a024b60f574b32014ba
5
5
  SHA512:
6
- metadata.gz: 595fdd5c5980a99bcec80b407780c3f12ba0ac6915130a7f371309cd0d49bcd15d0d64d4d31a84a7a5b25808a7ddba3b64af701aa69f78816b34596f1762ef39
7
- data.tar.gz: 90b47aff56495a7d15587aff63d673195a4bab761db7d34b12c3e05b3f825d7c7aaf0e3f98d90055a3871a465687a8127b5afd3bd44675ffdcfdf3e3209f3419
6
+ metadata.gz: 9413e5c3d0bf479d3022ce75bbbcd639fee2ffa6632883859419b724ff7cb9b04c792b4118837f854714611b2db1756ca3294ead1767fc9ab669c8c7e0b8806c
7
+ data.tar.gz: 4be7c88f9c2e1bdb45c70c45c90367afe2b1a8616837ca121a07a769521c8e1c1e3bb29224678d2425a49473db4b6549f4d71b9d9ca0e3e8f2eb0d3715e19138
data/.travis.yml CHANGED
@@ -4,7 +4,8 @@ sudo: false
4
4
  cache: bundler
5
5
  bundler_args: --without benchmarks tools
6
6
  script:
7
- - bundle exec rake spec
7
+ - bundle exec rake
8
+ - bundle exec rubocop
8
9
  rvm:
9
10
  - 2.3.0
10
11
  - 2.4.0
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 in Rails, or PORO [Dry::Validation][dry-validation]. All of them focus on providing rich DSL-s for **validation rules**.
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 validation error -- to allow filtering and sorting validation results.
22
- - By **composable** we mean a possibility to merge errors provided by one policy/validator to another, for building nested sets of well-focused policies.
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 message and a set of additional tags
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 "Title is empty", field: "title", level: "error"
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 "Subtitle is empty", field: "subtitle", level: "warning"
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. Policy initializer will perform all the checks immediately, memoizing the results into `errors` array. The methods `#valid?`, `#invalid?` and `#validate!` just check those `#errors`.
67
+ Because validation is the only responsibility of a policy, we don't need to call it explicitly.
68
68
 
69
- You can treat an instance of policy object as immutable.
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
- # Look at errors closer
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
- # Error messages are already added under special key :message
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
- # A shortcut
90
- policy.messages # => ["Subtitle is empty", "Error translation for missed text"]
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
- # More verbose strings
93
- policy.full_messages
166
+ # ...
167
+ policy.messages
94
168
  # => [
95
- # 'Subtitle is empty: {"field":"subtitle", "level":"warning"}'
96
- # 'Error translation for missed text: {"field":"text", "level":"error"}'
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
- # You can use tags in checkers -- to add condition for errors to ignore
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
- # Notice the `invalid` takes a block with definitions for errors to count (not ignore)
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.by_tags(level: "error")
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
- 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 concerns a scope for the translation. By default it is taken from the full name of current class prepended with `"tram-policy"`.
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
- ```ruby
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: self.class.send(:scope), **options
35
+ I18n.t message, scope: scope, **options
27
36
  end
28
37
 
29
- # Collection of validation errors
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**
@@ -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 ||= ["tram-policy", *Inflector.underscore(name)]
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
@@ -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
- return value if value.is_a? self
20
- super
19
+ value.instance_of?(self) ? value : super
21
20
  end
22
21
 
23
- # @!attribute [r] message
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 [String] The error message text
32
+ # @return [Array]
26
33
  #
27
- attr_reader :message
34
+ def item
35
+ [key, tags]
36
+ end
37
+ alias to_a item
28
38
 
29
- # The full message (message and tags info)
39
+ # The text of error message translated to the current locale
30
40
  #
31
41
  # @return [String]
32
42
  #
33
- def full_message
34
- [message, @tags].reject(&:empty?).join(" ")
43
+ def message
44
+ key.is_a?(Symbol) ? I18n.t(*item) : key.to_s
35
45
  end
36
46
 
37
- # Converts the error to a simple hash with message and tags
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
- @tags.merge(message: message)
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 either message or a tag
71
+ # Fetches an option
46
72
  #
47
73
  # @param [#to_sym] tag
48
74
  # @return [Object]
49
75
  #
50
76
  def [](tag)
51
- to_h[tag.to_sym]
77
+ tags[tag.to_sym]
52
78
  end
53
79
 
54
- # Fetches either message or a tag
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
- to_h.fetch(tag.to_sym, default, &block)
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 [#to_h]
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?(:to_h) && other.to_h == to_h
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(message, **tags)
77
- @message = message.to_s
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 : @tags[name]
129
+ args.any? || block ? super : tags[name]
87
130
  end
88
131
  end
89
132
  end
@@ -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 = nil, **tags)
25
- message ||= tags.delete(:message)
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 by_tags(filter)
42
- # Selects errors filtered by tags
39
+ # @!method filter(key = nil, tags)
40
+ # Filter errors by optional key and tags
43
41
  #
44
- # @param [Hash<Symbol, Object>] filter List of options to filter by
45
- # @return [Hash<Symbol, Object>]
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 by_tags(**filter)
48
- filter = filter.to_a
49
- reject { |error| (filter - error.to_h.to_a).any? }
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
- # The array of ordered error messages with error tags info
92
+ # @deprecated
93
+ # List of error descriptions
70
94
  #
71
95
  # @return [Array<String>]
72
96
  #
73
97
  def full_messages
74
- @set.map(&:full_message).sort
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
- new_err = block_given? ? yield(err.to_h) : err.to_h
94
- add new_err.merge(options)
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 = Set.new)
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
@@ -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&.by_tags(tags)
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(&:message)
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 { |error| text << " - #{error.full_message}\n" }
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(&:full_message)
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
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  en:
3
+ bad: Something bad has happened
3
4
  tram-policy:
5
+ bad: Something bad has happened
4
6
  test/customer_policy:
5
7
  name_presence: Name is absent
6
8
  test/user_policy:
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
- # Prepare the Test namespace for constants defined in specs
19
- config.before(:each) { Test = Class.new(Module) }
20
- config.after(:each) { Object.send :remove_const, :Test }
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 "Something bad happened", tags }
2
+ subject(:error) { described_class.new :bad, options }
3
3
 
4
- let(:tags) { { level: "warning" } }
4
+ let(:options) { { level: "warning", scope: %w[tram-policy] } }
5
5
 
6
- describe "#message" do
7
- subject { error.message }
8
- it { is_expected.to eq "Something bad happened" }
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 "#to_h" do
25
- subject { error.to_h }
26
- it { is_expected.to eq message: "Something bad happened", level: "warning" }
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 #to_h:" do
33
- let(:other) { double to_h: error.to_h }
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 #to_h:" do
38
- let(:other) { double to_h: error.to_h.merge(foo: :bar) }
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 #to_h:" do
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, t: "OMG!" }
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 { errors.add :omg, level: "info", field: "name" }
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).to eq message: "OMG!", level: "info", field: "name"
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.map(&:to_h)).to match_array [
52
- { message: "OMG!", level: "disaster" },
53
- { message: "OMG!", level: "error" }
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.map(&:to_h)).to match_array [
64
- { message: "OMG!", level: "disaster" },
65
- { message: "OMG!", level: "error", source: "Homer" }
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.map(&:to_h)).to match_array [
76
- { message: "OMG!", level: "disaster" },
77
- { message: "OMG!", level: "error", source: "Homer" }
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.map(&:to_h)).to match_array [
88
- { message: "OMG!", level: "disaster" },
89
- { message: "OMG!", level: "error", id: 5, age: 4 }
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 "not errors:" do
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
- it { is_expected.to eq [] }
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 "#by_tags" do
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.by_tags level: "error" }
124
+ subject { errors.filter level: "error" }
131
125
 
132
126
  it "returns selected errors only" do
133
- expect(subject.map(&:to_h)).to match_array [
134
- { message: "OMG!", field: "name", level: "error" },
135
- { message: "OMG!", field: "email", level: "error" }
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.by_tags }
135
+ subject { errors.filter }
142
136
 
143
137
  it "returns selected all errors" do
144
- expect(subject.map(&:to_h)).to match_array errors.to_a
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 :invalid_policy, field: "name" do
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 full_message: "OMG!", level: "error" }
5
- let(:two) { double full_message: "phew!", level: "warning" }
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.1"
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.3.1
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-01-05 00:00:00.000000000 Z
12
+ date: 2018-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dry-initializer