tram-validators 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +14 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/lib/tram-validators.rb +1 -0
- data/lib/tram/validators.rb +64 -0
- data/lib/tram/validators/consistency_validator.rb +26 -0
- data/lib/tram/validators/contract_validator.rb +14 -0
- data/lib/tram/validators/each_validator.rb +30 -0
- data/lib/tram/validators/outcome_validator.rb +50 -0
- data/lib/tram/validators/reference_validator.rb +14 -0
- data/lib/tram/validators/size_validator.rb +37 -0
- data/lib/tram/validators/validity_validator.rb +27 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/invalid_with_error.rb +10 -0
- data/spec/tram-validators/consistency_validator_spec.rb +131 -0
- data/spec/tram-validators/contract_validator_spec.rb +58 -0
- data/spec/tram-validators/each_validator_spec.rb +35 -0
- data/spec/tram-validators/outcome_validator_spec.rb +27 -0
- data/spec/tram-validators/size_validator_spec.rb +347 -0
- data/spec/tram-validators/validity_validator_spec.rb +58 -0
- data/tram-validators.gemspec +23 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: aca5b01edeaa35458d5ba907676b8bed73a60f1e
|
4
|
+
data.tar.gz: 9c65185129ee67af6848516c0623405676176383
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dcf7914965ea2df8011562c7dc85b4f0f5fd22fe388421558394fdbcb6e39ec6cc5fbb885f79dc0a024a7bc4016159770791c855db8ba51a44a8135af0bcaaf0
|
7
|
+
data.tar.gz: 61dd66c80636c75067a5c2eb438d682a3fae75a818113f905e5d806296a741e1243f516a544059ddae681d2206a2ca16e1460dbb34f472d4a4f64f33d0def8b6
|
data/.codeclimate.yml
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
DisplayCopNames: true
|
4
|
+
DisplayStyleGuide: true
|
5
|
+
StyleGuideCopsOnly: true
|
6
|
+
TargetRubyVersion: 2.3
|
7
|
+
|
8
|
+
Style/Alias:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Style/ClassAndModuleChildren:
|
12
|
+
EnforcedStyle: compact
|
13
|
+
|
14
|
+
Style/FileName:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/FrozenStringLiteralComment:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/ModuleFunction:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Style/StringLiterals:
|
24
|
+
EnforcedStyle: double_quotes
|
25
|
+
|
26
|
+
Style/StringLiteralsInInterpolation:
|
27
|
+
EnforcedStyle: double_quotes
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Andrew Kozin (nepalez), Evil Martians
|
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,134 @@
|
|
1
|
+
# Tram::Validators
|
2
|
+
|
3
|
+
Collection of ActiveModel validators for rails projects with focus on composition of standalone policy objects.
|
4
|
+
|
5
|
+
<a href="https://evilmartians.com/">
|
6
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
7
|
+
|
8
|
+
[![Gem Version][gem-badger]][gem]
|
9
|
+
[![Build Status][travis-badger]][travis]
|
10
|
+
[![Dependency Status][gemnasium-badger]][gemnasium]
|
11
|
+
[![Code Climate][codeclimate-badger]][codeclimate]
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem "tram-validators"
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Below is a short review of available validators. Read the [specs][specs] for more details:
|
22
|
+
|
23
|
+
### Contract Validator
|
24
|
+
|
25
|
+
Checks that a value satisfies a contract, represented by a standalone validator (policy object).
|
26
|
+
It applies policy validator, and collects its messages under corresponding keys.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class PolicyObject < SimpleDelegator
|
30
|
+
include ActiveModel::Validations
|
31
|
+
validates :bar, presence: true
|
32
|
+
end
|
33
|
+
|
34
|
+
# PolicyObject.new(record.foo).valid? == true
|
35
|
+
# collects original error messages from policy under the key `foo`
|
36
|
+
validates :foo, contract: { policy: PolicyObject }
|
37
|
+
|
38
|
+
# collects the same messages from policy under their original keys (`bar`)
|
39
|
+
validates :foo, contract: { policy: PolicyObject, original_keys: true }
|
40
|
+
|
41
|
+
# collects the same messages from policy under nested keys (`foo[bar]`)
|
42
|
+
validates :foo, contract: { policy: PolicyObject, nested_keys: true }
|
43
|
+
```
|
44
|
+
|
45
|
+
### Validity Validator
|
46
|
+
|
47
|
+
Checks that an attribute is valid per se.
|
48
|
+
It collects original error messages under corresponding keys.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# record.foo.valid? == true
|
52
|
+
# collects original error messages under the key `foo`
|
53
|
+
validates :foo, validity: true
|
54
|
+
|
55
|
+
# collects the same messages from policy under their original keys (`bar`)
|
56
|
+
validates :foo, validity: { original_keys: true }
|
57
|
+
|
58
|
+
# collects the same messages from policy under nested keys (`foo[bar]`)
|
59
|
+
validates :foo, validity: { nested_keys: true }
|
60
|
+
```
|
61
|
+
|
62
|
+
### Each Validator
|
63
|
+
|
64
|
+
Applies validation rule to every element of the collection (that responds to `to_a`).
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# Checks that every element of record.list is present
|
68
|
+
# collects original errors under the key `list[i]` (i for index of invalid item)
|
69
|
+
validates :list, each: { presence: true }
|
70
|
+
```
|
71
|
+
|
72
|
+
### Outcome Validator
|
73
|
+
|
74
|
+
Validates value by checking another method depending on it.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
# Validates `user_id` by checking that `user.role` (depending on user_id) is set
|
78
|
+
# adds error `user_role_presence` to the original attribute `user_id`
|
79
|
+
validates :user_id, outcome: { value: "user.role", presence: true }
|
80
|
+
```
|
81
|
+
|
82
|
+
This technics is useful in form objects where you should attach errors to the original fields accessible to the user.
|
83
|
+
|
84
|
+
### Consistency Validator
|
85
|
+
|
86
|
+
Compares a value of some attribute to a value of another attribute or method chain.
|
87
|
+
Supports all keys from the standard rails [numericality validator][numericality].
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# record.foo < record.bar.baz
|
91
|
+
# adds error named `less_than_bar_baz` under the key `foo`
|
92
|
+
validates :foo, consistency: { less_than: "bar.baz" }
|
93
|
+
```
|
94
|
+
|
95
|
+
### Size Validator
|
96
|
+
|
97
|
+
Compares size of array to given value or another attribute.
|
98
|
+
Supports all keys from the standard rails [numericality validator][numericality].
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# record.names.size < 6
|
102
|
+
# adds error named `size_less_than` under the key `names`
|
103
|
+
validates :names, size: { less_than: 6 }
|
104
|
+
|
105
|
+
# record.values.size == record.parent&.names&.size
|
106
|
+
# adds error named `size_equal_to_parent_names_size` under the key `names`
|
107
|
+
validates :values, size: { equal_to: "parent.names.size" }
|
108
|
+
```
|
109
|
+
|
110
|
+
### Reference Validator
|
111
|
+
|
112
|
+
This is an AR-dependent validator, that checks an instance can be extracted from database by given key.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# Checks that User.find_by(record.user_key).present?
|
116
|
+
validates :user_key, reference: { model: User, find_by: :key }
|
117
|
+
```
|
118
|
+
|
119
|
+
Like the outcome validator above, it can be useful for validation of form objects.
|
120
|
+
|
121
|
+
## License
|
122
|
+
|
123
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
124
|
+
|
125
|
+
[specs]: ./spec/tram-validators
|
126
|
+
[numericality]: http://guides.rubyonrails.org/active_record_validations.html#numericality
|
127
|
+
[codeclimate-badger]: https://img.shields.io/codeclimate/github/tram-rb/tram-validators.svg?style=flat
|
128
|
+
[codeclimate]: https://codeclimate.com/github/tram-rb/tram-validators
|
129
|
+
[gem-badger]: https://img.shields.io/gem/v/tram-validators.svg?style=flat
|
130
|
+
[gem]: https://rubygems.org/gems/tram-validators
|
131
|
+
[gemnasium-badger]: https://img.shields.io/gemnasium/tram-rb/tram-validators.svg?style=flat
|
132
|
+
[gemnasium]: https://gemnasium.com/tram-rb/tram-validators
|
133
|
+
[travis-badger]: https://img.shields.io/travis/tram-rb/tram-validators/master.svg?style=flat
|
134
|
+
[travis]: https://travis-ci.org/tram-rb/tram-validators
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "tram/validators"
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "active_model"
|
2
|
+
require "active_support/inflector"
|
3
|
+
|
4
|
+
module Tram
|
5
|
+
module Validators
|
6
|
+
# Standard conditions to check
|
7
|
+
CONDITIONS = {
|
8
|
+
equal_to: ->(value, limit) { value == limit },
|
9
|
+
other_than: ->(value, limit) { value != limit },
|
10
|
+
less_than: ->(value, limit) { value < limit },
|
11
|
+
less_than_or_equal_to: ->(value, limit) { value <= limit },
|
12
|
+
greater_than: ->(value, limit) { value > limit },
|
13
|
+
greater_than_or_equal_to: ->(value, limit) { value >= limit }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Gets value of chained attribute
|
18
|
+
def chained_value(record, chain)
|
19
|
+
chain.to_s.split(".").inject(record) { |obj, name| obj&.send(name) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Provides standard key for error collected by standalone validators
|
23
|
+
def error_key(source, target, nested_keys: nil, original_keys: nil, **)
|
24
|
+
return source if original_keys
|
25
|
+
return target unless nested_keys
|
26
|
+
source.to_s
|
27
|
+
.split(/\[|\]/)
|
28
|
+
.compact
|
29
|
+
.inject(target) { |obj, key| "#{obj}[#{key}]" }
|
30
|
+
.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
# Extracts nested validators from options
|
34
|
+
def validators(attr, options, *blacklist)
|
35
|
+
options.map.with_object({}) do |(key, opts), obj|
|
36
|
+
name = key.to_s
|
37
|
+
|
38
|
+
next if blacklist.map(&:to_s).include? name
|
39
|
+
next if %w(allow_nil if unless on message).include? name
|
40
|
+
|
41
|
+
opts = {} unless opts.is_a? Hash
|
42
|
+
obj[key] = find_validator_by_name(name).new(attributes: attr, **opts)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def find_validator_by_name(name)
|
49
|
+
klass_name = "#{name.camelize}Validator"
|
50
|
+
klass_name.constantize
|
51
|
+
rescue
|
52
|
+
ActiveModel::Validations.const_get(klass_name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
require_relative "validators/consistency_validator"
|
57
|
+
require_relative "validators/contract_validator"
|
58
|
+
require_relative "validators/each_validator"
|
59
|
+
require_relative "validators/outcome_validator"
|
60
|
+
require_relative "validators/reference_validator"
|
61
|
+
require_relative "validators/size_validator"
|
62
|
+
require_relative "validators/validity_validator"
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Compares value of the attribute to value of another attribute
|
2
|
+
#
|
3
|
+
# @example Compares price to source.price
|
4
|
+
# validates :price, consistency: { greater_than: 'source.price' }
|
5
|
+
# # I18n error key 'price.greater_than_source.price'
|
6
|
+
#
|
7
|
+
class ConsistencyValidator < ActiveModel::EachValidator
|
8
|
+
def validate_each(record, attribute, value)
|
9
|
+
Tram::Validators::CONDITIONS.each do |key, block|
|
10
|
+
check(key, record, attribute, value, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def check(condition, record, attribute, value)
|
17
|
+
chain = options[condition]
|
18
|
+
return if chain.blank?
|
19
|
+
|
20
|
+
other_value = Tram::Validators.chained_value(record, chain)
|
21
|
+
return if value && other_value && yield(value, other_value)
|
22
|
+
|
23
|
+
error_name = [condition, chain.to_s.split(".")].flatten.join("_").to_sym
|
24
|
+
record.errors.add attribute, error_name, value: value, other: other_value
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Validates attribute by calling given type with the attribute's value
|
2
|
+
#
|
3
|
+
# @example Checks that AdminPolicy.new(user).valid?
|
4
|
+
# validates :user, contract: { policy: AdminPolicy, nested_keys: true }
|
5
|
+
#
|
6
|
+
class ContractValidator < ActiveModel::EachValidator
|
7
|
+
def validate_each(record, attribute, value)
|
8
|
+
return unless options[:policy]
|
9
|
+
options[:policy].new(value).tap(&:valid?).errors.messages.each do |key, msg|
|
10
|
+
error_key = Tram::Validators.error_key(key, attribute, options)
|
11
|
+
msg.each { |message| record.errors.add error_key, message }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Applies validator to every element of the collection
|
2
|
+
class EachValidator < ActiveModel::EachValidator
|
3
|
+
def validate_each(record, attribute, values)
|
4
|
+
return unless values.is_a? Enumerable
|
5
|
+
values.each_with_index do |value, index|
|
6
|
+
item = record.dup.tap { |rec| rec.errors.clear }
|
7
|
+
|
8
|
+
call_validations(item, attribute, value)
|
9
|
+
|
10
|
+
copy_errors(item, record, attribute, index)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def call_validations(item, attribute, value)
|
17
|
+
Tram::Validators.validators(@attributes, options).each do |_, validator|
|
18
|
+
validator.validate_each item, attribute, value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def copy_errors(item, record, attribute, index)
|
23
|
+
item.errors.messages.each do |original_key, messages|
|
24
|
+
messages.each do |message|
|
25
|
+
key = original_key.to_s.sub /\A#{attribute}/, "#{attribute}[#{index}]"
|
26
|
+
record.errors.add key.to_sym, message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Validates attribute by applying validation rule not to the attribute itself,
|
2
|
+
# but to another parameter, whose value depends on the attribute.
|
3
|
+
#
|
4
|
+
# The resulting error is collected under the attribute's key, which
|
5
|
+
# is necessary to correctly bind the error to the field, that causes
|
6
|
+
# the problem.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# let(:user) { User.find_by(id: user_id) }
|
10
|
+
# validates :user_id, outcome: { value: 'user.name', presence: true }
|
11
|
+
# # user_id.user_name_presence
|
12
|
+
#
|
13
|
+
class OutcomeValidator < ActiveModel::EachValidator
|
14
|
+
def validate_each(record, attribute, value)
|
15
|
+
dependency = options[:value].to_s.gsub(".", "_")
|
16
|
+
dependent = Tram::Validators.chained_value(record, options[:value])
|
17
|
+
validators = Tram::Validators.validators(@attributes, options, :value)
|
18
|
+
|
19
|
+
sandbox = record.dup
|
20
|
+
validators.each do |condition, validator|
|
21
|
+
next if valid_in_sandbox(sandbox, attribute, dependent, validator)
|
22
|
+
|
23
|
+
key = message_key(dependency, condition)
|
24
|
+
text = message(record, attribute, value, dependent, key)
|
25
|
+
record.errors.add attribute, text
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def valid_in_sandbox(sandbox, attribute, dependent, validator)
|
32
|
+
sandbox.errors.clear
|
33
|
+
validator.validate_each(sandbox, attribute, dependent)
|
34
|
+
sandbox.errors.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def message_key(dependency, condition)
|
38
|
+
[dependency, condition.to_s].compact.join("_").to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
def message(record, attribute, value, dependent, message_key)
|
42
|
+
model = record.class.name.underscore
|
43
|
+
scope = %W(active_model errors models #{model} attributes #{attribute})
|
44
|
+
I18n.t message_key, record: record,
|
45
|
+
attribute: attribute,
|
46
|
+
value: value,
|
47
|
+
delependent: dependent,
|
48
|
+
scope: scope
|
49
|
+
end
|
50
|
+
end
|