error_normalizer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +90 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +6 -0
- data/error_normalizer.gemspec +29 -0
- data/lib/error_normalizer.rb +40 -0
- data/lib/error_normalizer/error.rb +63 -0
- data/lib/error_normalizer/message_parser.rb +97 -0
- data/lib/error_normalizer/normalizer.rb +110 -0
- data/lib/error_normalizer/version.rb +5 -0
- metadata +142 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3a2b14320f32c8b5b9709d2a7963941ff27cebe857533a3a38401e4ccb08709a
|
|
4
|
+
data.tar.gz: 5d9f38f16ea5aac77e3a0318eb4cacac7b9aab5d24574e8d55c5c3de4b3cb08f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ea13a01344de75bd4161ff70a70f268aaa2d6b393273f14e000da2b8213ca61eae426d12e97acdbe06eaf9f2dbbe4fa3d4427214e47ad46558de8422105dd058
|
|
7
|
+
data.tar.gz: 58fd33623c68e76af2bf94fbd7ec1cd8dea6a17baae99315bfddc687862c00e6f9dbdf38d3ebd6f83f69cb06b6c5232986548bac98c9e4ddb15baf4e4e118136
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Common configuration.
|
|
2
|
+
AllCops:
|
|
3
|
+
TargetRubyVersion: 2.5
|
|
4
|
+
TargetRailsVersion: 5.1
|
|
5
|
+
|
|
6
|
+
#################### Style ###########################
|
|
7
|
+
|
|
8
|
+
Style/Documentation:
|
|
9
|
+
Enabled: true
|
|
10
|
+
|
|
11
|
+
Style/DoubleNegation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/EmptyMethod:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Style/FrozenStringLiteralComment:
|
|
18
|
+
Enabled: true
|
|
19
|
+
|
|
20
|
+
Style/ModuleFunction:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
#################### Naming ##########################
|
|
24
|
+
|
|
25
|
+
Naming/AccessorMethodName:
|
|
26
|
+
Enabled: false
|
|
27
|
+
|
|
28
|
+
Naming/PredicateName:
|
|
29
|
+
Enabled: false
|
|
30
|
+
|
|
31
|
+
#################### Metrics #########################
|
|
32
|
+
|
|
33
|
+
Metrics/AbcSize:
|
|
34
|
+
Max: 15
|
|
35
|
+
|
|
36
|
+
Metrics/BlockLength:
|
|
37
|
+
CountComments: false
|
|
38
|
+
Max: 25
|
|
39
|
+
Exclude:
|
|
40
|
+
- 'error_normalizer.gemspec'
|
|
41
|
+
- 'spec/**/*_spec.rb'
|
|
42
|
+
|
|
43
|
+
Metrics/BlockNesting:
|
|
44
|
+
CountBlocks: false
|
|
45
|
+
Max: 3
|
|
46
|
+
|
|
47
|
+
Metrics/ClassLength:
|
|
48
|
+
CountComments: false
|
|
49
|
+
Max: 100
|
|
50
|
+
|
|
51
|
+
# Avoid complex methods.
|
|
52
|
+
Metrics/CyclomaticComplexity:
|
|
53
|
+
Max: 6
|
|
54
|
+
|
|
55
|
+
Metrics/LineLength:
|
|
56
|
+
Max: 100
|
|
57
|
+
# To make it possible to copy or click on URIs in the code, we allow lines
|
|
58
|
+
# containing a URI to be longer than Max.
|
|
59
|
+
AllowHeredoc: true
|
|
60
|
+
AllowURI: true
|
|
61
|
+
URISchemes:
|
|
62
|
+
- http
|
|
63
|
+
- https
|
|
64
|
+
# The IgnoreCopDirectives option causes the LineLength rule to ignore cop
|
|
65
|
+
# directives like '# rubocop: enable ...' when calculating a line's length.
|
|
66
|
+
IgnoreCopDirectives: true
|
|
67
|
+
# The IgnoredPatterns option is a list of !ruby/regexp and/or string
|
|
68
|
+
# elements. Strings will be converted to Regexp objects. A line that matches
|
|
69
|
+
# any regular expression listed in this option will be ignored by LineLength.
|
|
70
|
+
IgnoredPatterns: []
|
|
71
|
+
|
|
72
|
+
Metrics/MethodLength:
|
|
73
|
+
CountComments: false
|
|
74
|
+
Max: 12
|
|
75
|
+
|
|
76
|
+
Metrics/ModuleLength:
|
|
77
|
+
CountComments: false
|
|
78
|
+
Max: 200
|
|
79
|
+
|
|
80
|
+
Metrics/ParameterLists:
|
|
81
|
+
Max: 5
|
|
82
|
+
CountKeywordArgs: true
|
|
83
|
+
|
|
84
|
+
Metrics/PerceivedComplexity:
|
|
85
|
+
Max: 7
|
|
86
|
+
|
|
87
|
+
##################### Layout ##########################
|
|
88
|
+
|
|
89
|
+
Layout/MultilineMethodCallIndentation:
|
|
90
|
+
Enabled: false
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
error_normalizer (0.1.0)
|
|
5
|
+
dry-configurable (~> 0.7.0)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
ast (2.4.0)
|
|
11
|
+
concurrent-ruby (1.0.5)
|
|
12
|
+
diff-lcs (1.3)
|
|
13
|
+
dry-configurable (0.7.0)
|
|
14
|
+
concurrent-ruby (~> 1.0)
|
|
15
|
+
dry-container (0.6.0)
|
|
16
|
+
concurrent-ruby (~> 1.0)
|
|
17
|
+
dry-configurable (~> 0.1, >= 0.1.3)
|
|
18
|
+
dry-core (0.4.7)
|
|
19
|
+
concurrent-ruby (~> 1.0)
|
|
20
|
+
dry-equalizer (0.2.1)
|
|
21
|
+
dry-inflector (0.1.2)
|
|
22
|
+
dry-logic (0.4.2)
|
|
23
|
+
dry-container (~> 0.2, >= 0.2.6)
|
|
24
|
+
dry-core (~> 0.2)
|
|
25
|
+
dry-equalizer (~> 0.2)
|
|
26
|
+
dry-types (0.13.2)
|
|
27
|
+
concurrent-ruby (~> 1.0)
|
|
28
|
+
dry-container (~> 0.3)
|
|
29
|
+
dry-core (~> 0.4, >= 0.4.4)
|
|
30
|
+
dry-equalizer (~> 0.2)
|
|
31
|
+
dry-inflector (~> 0.1, >= 0.1.2)
|
|
32
|
+
dry-logic (~> 0.4, >= 0.4.2)
|
|
33
|
+
dry-validation (0.12.2)
|
|
34
|
+
concurrent-ruby (~> 1.0)
|
|
35
|
+
dry-configurable (~> 0.1, >= 0.1.3)
|
|
36
|
+
dry-core (~> 0.2, >= 0.2.1)
|
|
37
|
+
dry-equalizer (~> 0.2)
|
|
38
|
+
dry-logic (~> 0.4, >= 0.4.0)
|
|
39
|
+
dry-types (~> 0.13.1)
|
|
40
|
+
jaro_winkler (1.5.1)
|
|
41
|
+
parallel (1.12.1)
|
|
42
|
+
parser (2.5.1.2)
|
|
43
|
+
ast (~> 2.4.0)
|
|
44
|
+
powerpack (0.1.2)
|
|
45
|
+
rainbow (3.0.0)
|
|
46
|
+
rake (10.4.2)
|
|
47
|
+
rspec (3.8.0)
|
|
48
|
+
rspec-core (~> 3.8.0)
|
|
49
|
+
rspec-expectations (~> 3.8.0)
|
|
50
|
+
rspec-mocks (~> 3.8.0)
|
|
51
|
+
rspec-core (3.8.0)
|
|
52
|
+
rspec-support (~> 3.8.0)
|
|
53
|
+
rspec-expectations (3.8.1)
|
|
54
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
55
|
+
rspec-support (~> 3.8.0)
|
|
56
|
+
rspec-mocks (3.8.0)
|
|
57
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
58
|
+
rspec-support (~> 3.8.0)
|
|
59
|
+
rspec-support (3.8.0)
|
|
60
|
+
rubocop (0.59.2)
|
|
61
|
+
jaro_winkler (~> 1.5.1)
|
|
62
|
+
parallel (~> 1.10)
|
|
63
|
+
parser (>= 2.5, != 2.5.1.1)
|
|
64
|
+
powerpack (~> 0.1)
|
|
65
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
66
|
+
ruby-progressbar (~> 1.7)
|
|
67
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
|
68
|
+
ruby-progressbar (1.10.0)
|
|
69
|
+
unicode-display_width (1.4.0)
|
|
70
|
+
|
|
71
|
+
PLATFORMS
|
|
72
|
+
ruby
|
|
73
|
+
|
|
74
|
+
DEPENDENCIES
|
|
75
|
+
bundler (~> 1.16)
|
|
76
|
+
dry-validation (~> 0.12.2)
|
|
77
|
+
error_normalizer!
|
|
78
|
+
rake (~> 10.0)
|
|
79
|
+
rspec (~> 3.0)
|
|
80
|
+
rubocop (= 0.59.2)
|
|
81
|
+
|
|
82
|
+
BUNDLED WITH
|
|
83
|
+
1.16.6
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 dikond
|
|
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,187 @@
|
|
|
1
|
+
# Hola :call_me_hand:
|
|
2
|
+
|
|
3
|
+
This gem was born out of need to have a establish a single universal error format to be consumed by frontend (JS), Android and iOS clients.
|
|
4
|
+
|
|
5
|
+
## API error format
|
|
6
|
+
|
|
7
|
+
In our projects we have a convention, that in case of a failed request (422) backend shall return a JSON response which conforms to the following schema:
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
errors: [{
|
|
11
|
+
key: 'has_already_been_taken',
|
|
12
|
+
type: 'params',
|
|
13
|
+
message: 'has already been taken',
|
|
14
|
+
payload: {
|
|
15
|
+
path: 'user.email'
|
|
16
|
+
}
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Each error object **must have** 4 required fields: `key`, `type`, `message` and `payload`.
|
|
21
|
+
|
|
22
|
+
- `key` is a concise error code that will be used in a user-friendly translations
|
|
23
|
+
- `type` may be `params`, `custom` or something else
|
|
24
|
+
- `params` means that some parameter that the backend received was wrong
|
|
25
|
+
- `custom` covers everything else from the business validation composed from several parameters to something really special
|
|
26
|
+
- `message` is a "default" or "fallback" error message in plain English that may be used if client does not have translation for the error code
|
|
27
|
+
- `payload` contains other useful data that assists client error handling. For example, in case of `type: "params"` we can provide a _path_ to the invalid paramter.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### dry-validation
|
|
32
|
+
|
|
33
|
+
GIVEN following [dry-validation](https://dry-rb.org/gems/dry-validation/) schema
|
|
34
|
+
|
|
35
|
+
schema = Dry::Validation.Schema do
|
|
36
|
+
required(:name).filled(size?: 3..15)
|
|
37
|
+
required(:email).filled(format?: /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
|
38
|
+
optional(:credit_card).filled(:bool?)
|
|
39
|
+
optional(:cash).filled(:bool?)
|
|
40
|
+
|
|
41
|
+
rule(payment: [:credit_card, :cash]) do |card, cash|
|
|
42
|
+
card.eql?(true) ^ cash.eql?(true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
AND following input
|
|
47
|
+
|
|
48
|
+
errors = schema.call(name: 'DK', email: 'dk<@>dark.net').errors
|
|
49
|
+
# {:name=>["length must be within 3 - 15"], :credit_card=>["must be filled"]}
|
|
50
|
+
|
|
51
|
+
THEN we can convert given errors to API error format
|
|
52
|
+
|
|
53
|
+
ErrorNormalizer.normalize(errors)
|
|
54
|
+
# [{
|
|
55
|
+
# :key=>"length_must_be_within",
|
|
56
|
+
# :message=>"length must be within 3 - 15",
|
|
57
|
+
# :payload=>{:path=>"name", :range=>["3", "15"]},
|
|
58
|
+
# :type=>"params"
|
|
59
|
+
# }, {
|
|
60
|
+
# :key=>"is_in_invalid_format",
|
|
61
|
+
# :message=>"is in invalid format",
|
|
62
|
+
# :payload=>{:path=>"email"},
|
|
63
|
+
# :type=>"params"
|
|
64
|
+
# }, {
|
|
65
|
+
# :key=>"must_be_equal_to",
|
|
66
|
+
# :message=>"must be equal to true",
|
|
67
|
+
# :payload=>{:path=>"payment", :value=>"true"},
|
|
68
|
+
# :type=>"params"
|
|
69
|
+
# }]
|
|
70
|
+
|
|
71
|
+
For more information about supported errors and how they would be parsed please check the spec.
|
|
72
|
+
|
|
73
|
+
#### Type-inference feature
|
|
74
|
+
|
|
75
|
+
**TL;DR:** Add `_rule` to the [custom validation block](https://dry-rb.org/gems/dry-validation/custom-validation-blocks/) names (adding this to the [high-level rules](https://dry-rb.org/gems/dry-validation/high-level-rules/) won't harm either, praise the consistency!).
|
|
76
|
+
|
|
77
|
+
**Long version**: When you're using [custom validation blocks](https://dry-rb.org/gems/dry-validation/custom-validation-blocks/) the error output is slightly diffenet. Instead of attribute name it will have a rule name as a key. For example, GIVEN this schema
|
|
78
|
+
|
|
79
|
+
schema = Dry::Validation.Schema do
|
|
80
|
+
configure do
|
|
81
|
+
def self.messages
|
|
82
|
+
super.merge(en: { errors: { email_required: 'provide email' } })
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
required(:email).maybe(:str?)
|
|
87
|
+
required(:newsletter).value(:bool?)
|
|
88
|
+
|
|
89
|
+
validate(email_required: %i[newsletter email]) do |newsletter, email|
|
|
90
|
+
if newsletter == true
|
|
91
|
+
!email.nil?
|
|
92
|
+
else
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
AND the following input
|
|
99
|
+
|
|
100
|
+
errors = schema.call(newsletter: true, email: nil).errors
|
|
101
|
+
# { email_required: ['provide email'] }
|
|
102
|
+
|
|
103
|
+
THEN we will get following format after normalization
|
|
104
|
+
|
|
105
|
+
ErrorNormalizer.normalie(errors)
|
|
106
|
+
# [{
|
|
107
|
+
# key: 'provide_email',
|
|
108
|
+
# message: 'provide email',
|
|
109
|
+
# payload: { path: 'email_required' }, # should be empty to not confuse ppl
|
|
110
|
+
# type: 'params' # should be "rule" or "custom" but definately not "params"
|
|
111
|
+
# }]
|
|
112
|
+
|
|
113
|
+
The solution to this problem would be to use _type inference from the rule name_ feature. Just add a `_rule` to the name of a custom block validation, like this
|
|
114
|
+
|
|
115
|
+
validate(email_required_rule: %i[newsletter email]) do |newsletter, email|
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Now validation will produce errors like this
|
|
120
|
+
|
|
121
|
+
{ email_required_rule: ['provide email'] }
|
|
122
|
+
|
|
123
|
+
But we can easily spot keys which end with `_rule` and normalize such erros appropiately to the following format
|
|
124
|
+
|
|
125
|
+
ErrorNormalizer.normalize(email_required_rule: ['provide email'])
|
|
126
|
+
# [{
|
|
127
|
+
# key: 'provide_email',
|
|
128
|
+
# message: 'provide email',
|
|
129
|
+
# payload: {},
|
|
130
|
+
# type: 'rule'
|
|
131
|
+
# }]
|
|
132
|
+
|
|
133
|
+
You can customize rule name match pattern, type name or turn off this feature completely by specifying it in configuration block
|
|
134
|
+
|
|
135
|
+
ErrorNormalizer.configure do |config|
|
|
136
|
+
config.infer_type_from_rule_name = true
|
|
137
|
+
config.rule_matcher = /_rule\z/
|
|
138
|
+
config.type_name = 'rule'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
### ActiveModel::Validations
|
|
142
|
+
|
|
143
|
+
ActiveModel errors aren't fully supported. By that I mean errors will be converted to the single format, however you won't see really unique error `key` or `payload` with additional info.
|
|
144
|
+
|
|
145
|
+
GIVEN we have a model like this
|
|
146
|
+
|
|
147
|
+
class TestUser
|
|
148
|
+
include ActiveModel::Validations
|
|
149
|
+
|
|
150
|
+
attr_reader :name, :email
|
|
151
|
+
def initialize(name:, email:)
|
|
152
|
+
@name = name
|
|
153
|
+
@email = email
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
validates :name, presence: true, length: { in: 3..15 }
|
|
157
|
+
validates :email, presence: true, format: { with: /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
AND initialzied object with invalid data
|
|
161
|
+
|
|
162
|
+
user = TestUser.new(name: 'DK', email: 'dk<@>dark.net').tap(&:validate)
|
|
163
|
+
|
|
164
|
+
THEN we can normalize object errors to API error format
|
|
165
|
+
|
|
166
|
+
ErrorNormalizer.normalize(user.errors.to_hash)
|
|
167
|
+
# [{
|
|
168
|
+
# :key=>"is_too_short_minimum_is_3_characters",
|
|
169
|
+
# :message=>"is too short (minimum is 3 characters)",
|
|
170
|
+
# :payload=>{:path=>"name"},
|
|
171
|
+
# :type=>"params"
|
|
172
|
+
# }, {
|
|
173
|
+
# :key=>"is_invalid",
|
|
174
|
+
# :message=>"is invalid",
|
|
175
|
+
# :payload=>{:path=>"email"},
|
|
176
|
+
# :type=>"params"
|
|
177
|
+
# }]
|
|
178
|
+
|
|
179
|
+
## TODO
|
|
180
|
+
|
|
181
|
+
- plugin to make full error translation
|
|
182
|
+
- configure Gitlab CI
|
|
183
|
+
- parse ActiveModel error mesasges
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/error_normalizer/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'error_normalizer'
|
|
7
|
+
spec.version = ErrorNormalizer::VERSION
|
|
8
|
+
spec.authors = ['Denis Kondratenko']
|
|
9
|
+
spec.email = ['di.kondratenko@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Normalize dry-validation and ActiveModel errors to the universal format'
|
|
12
|
+
spec.homepage = 'https://gitlab.yalantis.com/web/error_normalizer'
|
|
13
|
+
spec.license = 'MIT'
|
|
14
|
+
|
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
19
|
+
end
|
|
20
|
+
spec.require_paths = ['lib']
|
|
21
|
+
|
|
22
|
+
spec.add_runtime_dependency 'dry-configurable', '~> 0.7.0'
|
|
23
|
+
|
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
|
25
|
+
spec.add_development_dependency 'dry-validation', '~> 0.12.2'
|
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
28
|
+
spec.add_development_dependency 'rubocop', '0.59.2'
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-configurable'
|
|
4
|
+
require 'error_normalizer/version'
|
|
5
|
+
require 'error_normalizer/normalizer'
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# Intended to normalize errors to the single format:
|
|
9
|
+
#
|
|
10
|
+
# {
|
|
11
|
+
# key: 'has_already_been_taken',
|
|
12
|
+
# type: 'params',
|
|
13
|
+
# message: 'has already been taken',
|
|
14
|
+
# payload: {
|
|
15
|
+
# path: 'user.email'
|
|
16
|
+
# }
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# We shall be able to automatically convert dry-validation output to this format
|
|
20
|
+
# and since we're using rails also automatically convert ActiveModel::Errors.
|
|
21
|
+
#
|
|
22
|
+
class ErrorNormalizer
|
|
23
|
+
extend Dry::Configurable
|
|
24
|
+
|
|
25
|
+
setting :infer_type_from_rule_name, true
|
|
26
|
+
setting :rule_matcher, /_rule\z/
|
|
27
|
+
setting :type_name, 'rule'
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Normalize errors to flat array of structured errors.
|
|
31
|
+
#
|
|
32
|
+
# @param input [Hash]
|
|
33
|
+
# @param opts [Hash] for list of supported options check ErrorNormalizer::Normalizer#new
|
|
34
|
+
# @return [Array<Hash>]
|
|
35
|
+
#
|
|
36
|
+
def self.normalize(input, **opts)
|
|
37
|
+
defaults = config.to_hash
|
|
38
|
+
Normalizer.new(input, defaults.merge(opts)).normalize.to_a
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ErrorNormalizer
|
|
4
|
+
# Error struct which makes cosmetic normalization
|
|
5
|
+
# upon calling either #to_hash and #to_json.
|
|
6
|
+
# Supports case equality check (#===) for hash structs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# Error.new('not_plausible', message: "can't recognize your phone", path: 'user.phone')
|
|
10
|
+
# Error.new('not_authorized').to_hash
|
|
11
|
+
# #=> { key: 'not_authorized', message: 'not authorized', type: 'params', payload: {} }
|
|
12
|
+
#
|
|
13
|
+
# # case equality works with hashes
|
|
14
|
+
# err = { key: 'err', message: 'err', type: 'custom', payload: {} }
|
|
15
|
+
# message =
|
|
16
|
+
# case err
|
|
17
|
+
# when Error
|
|
18
|
+
# 'YEP'
|
|
19
|
+
# else
|
|
20
|
+
# 'NOPE'
|
|
21
|
+
# end
|
|
22
|
+
# puts message #=> 'YEP'
|
|
23
|
+
#
|
|
24
|
+
class Error
|
|
25
|
+
def initialize(error_key, message: nil, type: 'params', **payload)
|
|
26
|
+
@key = error_key
|
|
27
|
+
@message = message
|
|
28
|
+
@type = type
|
|
29
|
+
@payload = payload
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.===(other)
|
|
33
|
+
return true if other.is_a?(Error)
|
|
34
|
+
return false unless other.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
h = other.transform_keys(&:to_s)
|
|
37
|
+
h.key?('key') & h.key?('message') && h.key?('payload') && h.key?('type')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_hash
|
|
41
|
+
{
|
|
42
|
+
key: @key,
|
|
43
|
+
message: message,
|
|
44
|
+
payload: payload,
|
|
45
|
+
type: @type
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_json
|
|
50
|
+
to_hash.to_json
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def message
|
|
56
|
+
@message || @key.tr('_', ' ')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def payload
|
|
60
|
+
@payload.delete_if { |_k, v| v.nil? || v.empty? }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ErrorNormalizer
|
|
4
|
+
#
|
|
5
|
+
# Parse error messages and extract payload metadata.
|
|
6
|
+
#
|
|
7
|
+
# ActiveModel ignored for now because we don't plan to use its validations.
|
|
8
|
+
# In case message isn't recognized we set error to be a simple
|
|
9
|
+
# normalized message (no spaces and special characters).
|
|
10
|
+
#
|
|
11
|
+
# Here are the links to AM::Errors and Dry::Validation list of error messages:
|
|
12
|
+
# - Dry: https://github.com/dry-rb/dry-validation/blob/8417e8/config/errors.yml
|
|
13
|
+
# - AM: https://github.com/svenfuchs/rails-i18n/blob/70b38b/rails/locale/en-US.yml#L111
|
|
14
|
+
#
|
|
15
|
+
class MessageParser
|
|
16
|
+
VALUE_MATCHERS = [
|
|
17
|
+
/\A(?<err>must not include) (?<val>.+)/,
|
|
18
|
+
/\A(?<err>must be equal to) (?<val>.+)/,
|
|
19
|
+
/\A(?<err>must not be equal to) (?<val>.+)/,
|
|
20
|
+
/\A(?<err>must be greater than) (?<val>\d+)/,
|
|
21
|
+
/\A(?<err>must be greater than or equal to) (?<val>\d+)/,
|
|
22
|
+
/\A(?<err>must include) (?<val>.+)/,
|
|
23
|
+
/\A(?<err>must be less than) (?<val>\d+)/,
|
|
24
|
+
/\A(?<err>must be less than or equal to) (?<val>\d+)/,
|
|
25
|
+
/\A(?<err>size cannot be greater than) (?<val>\d+)/,
|
|
26
|
+
/\A(?<err>size cannot be less than) (?<val>\d+)/,
|
|
27
|
+
/\A(?<err>size must be) (?<val>\d+)/,
|
|
28
|
+
/\A(?<err>length must be) (?<val>\d+)/
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
LIST_MATCHERS = [
|
|
32
|
+
/\A(?<err>must not be one of): (?<val>.+)/,
|
|
33
|
+
/\A(?<err>must be one of): (?<val>.+)/,
|
|
34
|
+
/\A(?<err>size must be within) (?<val>.+)/,
|
|
35
|
+
/\A(?<err>length must be within) (?<val>.+)/
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(message)
|
|
39
|
+
@message = message
|
|
40
|
+
@key = nil
|
|
41
|
+
@payload = {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse
|
|
45
|
+
parse_value_message
|
|
46
|
+
return to_a if @key
|
|
47
|
+
|
|
48
|
+
parse_list_message
|
|
49
|
+
return to_a if @key
|
|
50
|
+
|
|
51
|
+
@key = to_key(@message)
|
|
52
|
+
to_a
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_a
|
|
56
|
+
[@key, @message, @payload]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def parse_value_message
|
|
62
|
+
VALUE_MATCHERS.each do |matcher|
|
|
63
|
+
data = matcher.match(@message)
|
|
64
|
+
next if data.nil?
|
|
65
|
+
|
|
66
|
+
@key = to_key(data[:err])
|
|
67
|
+
@payload[:value] = data[:val]
|
|
68
|
+
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_list_message
|
|
74
|
+
LIST_MATCHERS.each do |matcher|
|
|
75
|
+
data = matcher.match(@message)
|
|
76
|
+
next if data.nil?
|
|
77
|
+
|
|
78
|
+
@key = to_key(data[:err])
|
|
79
|
+
@payload.merge!(parse_list_payload(data[:val]))
|
|
80
|
+
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_key(msg)
|
|
86
|
+
msg.downcase.tr(' ', '_').gsub(/[^a-z0-9_]/, '')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_list_payload(str)
|
|
90
|
+
if str.include?(' - ')
|
|
91
|
+
{ range: str.split(' - ') }
|
|
92
|
+
else
|
|
93
|
+
{ list: str.split(', ') }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'message_parser'
|
|
5
|
+
|
|
6
|
+
class ErrorNormalizer
|
|
7
|
+
#
|
|
8
|
+
# Responsible for converting input to the array of normalized errors.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# errors = { phone: ['not plausible'] }
|
|
12
|
+
# ErrorNormalizer::Normalizer.new(errors, namespace: 'customer').normalize
|
|
13
|
+
# # [{
|
|
14
|
+
# # key: 'not_plausible',
|
|
15
|
+
# # message: 'not plausible',
|
|
16
|
+
# # payload: { path: 'customer.phone' },
|
|
17
|
+
# # type: 'params'
|
|
18
|
+
# # }]
|
|
19
|
+
#
|
|
20
|
+
class Normalizer
|
|
21
|
+
UnsupportedInputType = Class.new(StandardError)
|
|
22
|
+
|
|
23
|
+
attr_reader :errors
|
|
24
|
+
|
|
25
|
+
def initialize(input, namespace: nil, **config)
|
|
26
|
+
@input = input
|
|
27
|
+
@namespace = namespace
|
|
28
|
+
@errors = []
|
|
29
|
+
@config = config
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_error(error, path: nil, **options)
|
|
33
|
+
@errors <<
|
|
34
|
+
case error
|
|
35
|
+
when Error
|
|
36
|
+
error
|
|
37
|
+
when Symbol, String
|
|
38
|
+
parse_error(error, path, options)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize
|
|
43
|
+
case @input
|
|
44
|
+
when Hash
|
|
45
|
+
normalize_hash(@input.dup)
|
|
46
|
+
when ActiveModel::Errors
|
|
47
|
+
normalize_hash(@input.to_hash)
|
|
48
|
+
else
|
|
49
|
+
raise "Don't know how to normalize errors"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_a
|
|
56
|
+
@errors.map(&:to_hash)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize_hash(input) # rubocop:disable AbcSize
|
|
62
|
+
return add_error(input) if input.is_a?(Error)
|
|
63
|
+
|
|
64
|
+
input.each do |key, value|
|
|
65
|
+
if messages_ary?(value)
|
|
66
|
+
options = prepare_error_options(key)
|
|
67
|
+
value.each { |msg| add_error(msg, options) }
|
|
68
|
+
elsif value.is_a?(Hash)
|
|
69
|
+
ns = namespaced_path(key)
|
|
70
|
+
Normalizer.new(value, namespace: ns).normalize.errors.each { |e| add_error(e) }
|
|
71
|
+
else
|
|
72
|
+
raise UnsupportedInputType
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def messages_ary?(ary)
|
|
78
|
+
return false unless ary.is_a? Array
|
|
79
|
+
|
|
80
|
+
ary.all? { |v| v.is_a? String }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_error(err_message, path, options)
|
|
84
|
+
result = MessageParser.new(err_message).parse
|
|
85
|
+
key, msg, payload = result.to_a
|
|
86
|
+
|
|
87
|
+
Error.new(key, message: msg, path: namespaced_path(path), **payload, **options)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def namespaced_path(path)
|
|
91
|
+
return if path.nil?
|
|
92
|
+
return path.to_s if @namespace.nil?
|
|
93
|
+
|
|
94
|
+
[@namespace, path].compact.join('.')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def prepare_error_options(key)
|
|
98
|
+
type = 'params'
|
|
99
|
+
payload = {}
|
|
100
|
+
|
|
101
|
+
if @config[:infer_type_from_rule_name] && @config[:rule_matcher].match?(key)
|
|
102
|
+
type = @config[:type_name]
|
|
103
|
+
else
|
|
104
|
+
payload = { path: key }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
payload.merge!(type: type)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: error_normalizer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Denis Kondratenko
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-11-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dry-configurable
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.7.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.7.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.16'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.16'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: dry-validation
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.12.2
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.12.2
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '10.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '10.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - '='
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 0.59.2
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - '='
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 0.59.2
|
|
97
|
+
description:
|
|
98
|
+
email:
|
|
99
|
+
- di.kondratenko@gmail.com
|
|
100
|
+
executables: []
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- ".gitignore"
|
|
105
|
+
- ".rspec"
|
|
106
|
+
- ".rubocop.yml"
|
|
107
|
+
- Gemfile
|
|
108
|
+
- Gemfile.lock
|
|
109
|
+
- LICENSE.txt
|
|
110
|
+
- README.md
|
|
111
|
+
- Rakefile
|
|
112
|
+
- error_normalizer.gemspec
|
|
113
|
+
- lib/error_normalizer.rb
|
|
114
|
+
- lib/error_normalizer/error.rb
|
|
115
|
+
- lib/error_normalizer/message_parser.rb
|
|
116
|
+
- lib/error_normalizer/normalizer.rb
|
|
117
|
+
- lib/error_normalizer/version.rb
|
|
118
|
+
homepage: https://gitlab.yalantis.com/web/error_normalizer
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata: {}
|
|
122
|
+
post_install_message:
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubyforge_project:
|
|
138
|
+
rubygems_version: 2.7.7
|
|
139
|
+
signing_key:
|
|
140
|
+
specification_version: 4
|
|
141
|
+
summary: Normalize dry-validation and ActiveModel errors to the universal format
|
|
142
|
+
test_files: []
|