subvalid 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/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +7 -0
- data/lib/subvalid/validation_result.rb +65 -0
- data/lib/subvalid/validator.rb +81 -0
- data/lib/subvalid/validator_registry.rb +18 -0
- data/lib/subvalid/validators/format_validator.rb +22 -0
- data/lib/subvalid/validators/in_validator.rb +22 -0
- data/lib/subvalid/validators/length_validator.rb +20 -0
- data/lib/subvalid/validators/numericality_validator.rb +19 -0
- data/lib/subvalid/validators/presence_validator.rb +18 -0
- data/lib/subvalid/validators/with_validator.rb +17 -0
- data/lib/subvalid/validators.rb +10 -0
- data/lib/subvalid/version.rb +3 -0
- data/lib/subvalid.rb +8 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/subvalid/validation_result_spec.rb +159 -0
- data/spec/subvalid/validator_registry_spec.rb +28 -0
- data/spec/subvalid/validator_spec.rb +61 -0
- data/spec/subvalid_spec.rb +4 -0
- data/subvalid.gemspec +25 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 19c0fec940d79b907b072f1df373e0b6add70f91
|
4
|
+
data.tar.gz: f2993841ad0edc5f065280f9518ed9e0df8d1a19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f41942e32113e9101e847c787e14fee98ec9b8a50baa6ddaba10d5e25b6ae73bf20b18d6df5fcdcdfae27cda032d24641d19f2452c9323fcbf5ec5e64f628c38
|
7
|
+
data.tar.gz: 4ecd389d43b363882c2378b89b649be4baf2e0dde5ff5df31812b13a8b28bd8b0a813462867df94ce2e2a80fb42d9c8b93c775bddec60cc7d9a59f83cb2aad81
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Envato
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Subvalid
|
2
|
+
|
3
|
+
Subvalid decouples your validation logic from your object structure. With
|
4
|
+
Subvalid you can define different validation rules for different contexts. So
|
5
|
+
rather than defining validation on the object, and having it be _"objective"_,
|
6
|
+
you can define it in a separate class - so it's _"subjective"_. (as in **Sub**jective
|
7
|
+
**valid**ation).
|
8
|
+
|
9
|
+
Subvalid was extracted from a project at [Envato](http://envato.com) which
|
10
|
+
required complex validation logic at each stage of the object's life cycle:
|
11
|
+
- Users upload videos. The videos are validated to make sure an actual
|
12
|
+
video was uploaded (and not someone's university Powerpoint slides), that
|
13
|
+
framerate is good, resolution and codec is acceptable etc. Failure here would
|
14
|
+
reject the file straight away, and tell the user to try again with a new file.
|
15
|
+
- Next we generate thumbnails, resized preview videos etc. If anything fails
|
16
|
+
validation here, it's a bug (wrong preview video size etc) - and we want to
|
17
|
+
alert developers.
|
18
|
+
- After the video is uploaded and processed, users would enter metadata: title,
|
19
|
+
description, tags etc. If that fails - we still want to save the item, but
|
20
|
+
just leave it as _"incomplete"_, and allow the user to come back later and
|
21
|
+
complete it. Once this passes, the item is ready, and we submit it for review to
|
22
|
+
our internal review team.
|
23
|
+
|
24
|
+
While
|
25
|
+
[ActiveModel::Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations.html)
|
26
|
+
is great if you've got simple validation logic, it doesn't cut it for something
|
27
|
+
complex like this. When you have different validation for the same object at
|
28
|
+
each point in it's life cycle, you need something more flexible.
|
29
|
+
|
30
|
+
ActiveModel also hooks in pretty deep into
|
31
|
+
[ActiveRecord](http://guides.rubyonrails.org/active_record_validations.html).
|
32
|
+
It's main use case assume you're just wanting to prevent bad data hitting your
|
33
|
+
database - which isn't necessarily always the case.
|
34
|
+
|
35
|
+
We needed something more. So Subvalid was born.
|
36
|
+
|
37
|
+
And you can have the best of both worlds. Subvalid can exist alongside
|
38
|
+
ActiveModel. ActiveModel::Validations is great for ensuring data consistency,
|
39
|
+
and you can add it to your model classes as normal - and then write Subvalid
|
40
|
+
validator classes in addition to handle more complex nuanced validation logic.
|
41
|
+
Or do it all in Subvalid - up to you.
|
42
|
+
|
43
|
+
## Features
|
44
|
+
- Very simple, consistent API
|
45
|
+
- Validation logic is defined in separate _"Validator"_ classes completely
|
46
|
+
decoupled from business logic
|
47
|
+
- Multiple validators can be defined for each piece of data in your system to be
|
48
|
+
executed at different points
|
49
|
+
- Caller is in control. No magic happening under the hood
|
50
|
+
- Failing validation does not block saving to the database
|
51
|
+
- Does not add _anything_ at all to business objects. No including modules, no
|
52
|
+
monkey patching, no object extension. Subvalid assumes POROs, but works with
|
53
|
+
anything. A key design goal is to **not** pollute the objects being validated at
|
54
|
+
all
|
55
|
+
- Supports nested validation on nested object structures - and nicely handles
|
56
|
+
nested errors.
|
57
|
+
|
58
|
+
## Development Status [![travis ci build](https://api.travis-ci.org/envato/subvalid.svg)](https://travis-ci.org/envato/subvalid)
|
59
|
+
|
60
|
+
Subvalid is extracted from production code in use at Envato. However, it is undergoing early development, and APIs and features are almost certain to be in flux.
|
61
|
+
|
62
|
+
## Getting Started
|
63
|
+
|
64
|
+
Add this line to your application's Gemfile:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
gem 'subvalid'
|
68
|
+
```
|
69
|
+
|
70
|
+
And then execute:
|
71
|
+
|
72
|
+
$ bundle
|
73
|
+
|
74
|
+
Or install it yourself as:
|
75
|
+
|
76
|
+
$ gem install subvalid
|
77
|
+
|
78
|
+
## Usage
|
79
|
+
|
80
|
+
TODO: Write usage instructions here
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it ( https://github.com/[my-github-username]/subvalid/fork )
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Subvalid
|
2
|
+
class ValidationResult
|
3
|
+
attr_reader :errors, :children
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@errors = []
|
7
|
+
@children = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
errors.empty? && children.values.all?(&:valid?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_error(error)
|
15
|
+
errors << error
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](attribute)
|
19
|
+
children[attribute]
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge_child!(attribute, result)
|
23
|
+
child = children[attribute]
|
24
|
+
if child
|
25
|
+
child.merge!(result)
|
26
|
+
else
|
27
|
+
children[attribute] = result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_h
|
32
|
+
hash = {}
|
33
|
+
hash[:errors] = errors.dup unless errors.empty?
|
34
|
+
children.each do |attribute, child|
|
35
|
+
hash[attribute] = child.to_h unless child.valid?
|
36
|
+
end
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def flatten(parent_attributes=[])
|
41
|
+
# TODO make this more flexible than just hardcoded format
|
42
|
+
flat_errors = errors.map{|error|
|
43
|
+
if parent_attributes.empty?
|
44
|
+
error
|
45
|
+
else
|
46
|
+
human_keys = parent_attributes.map{|a| a.to_s.gsub('_', ' ')}
|
47
|
+
[human_keys.join(", "), error].join(": ")
|
48
|
+
end
|
49
|
+
}
|
50
|
+
children.each do |attribute, child|
|
51
|
+
flat_errors += child.flatten(parent_attributes + [attribute])
|
52
|
+
end
|
53
|
+
flat_errors
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
def merge!(result)
|
58
|
+
@errors += result.errors
|
59
|
+
children.merge!(result.children){|key, old_child, new_child|
|
60
|
+
old_child.merge!(new_child)
|
61
|
+
old_child
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validator
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
extend DSL
|
6
|
+
end
|
7
|
+
base.extend API
|
8
|
+
end
|
9
|
+
|
10
|
+
module DSL
|
11
|
+
|
12
|
+
def validates(*attributes, **validators, &block)
|
13
|
+
if validators.empty? && !block
|
14
|
+
raise "no validations or block specified"
|
15
|
+
end
|
16
|
+
|
17
|
+
attributes = [:base] if attributes.empty?
|
18
|
+
|
19
|
+
add_validations(attributes, validators, block)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
ValidatorEntry = Struct.new(:validator, :args)
|
24
|
+
def validations
|
25
|
+
@validations ||= Hash.new{|vals,attribute| vals[attribute] = [] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_validations(attributes, validators, block)
|
29
|
+
attributes.each do |attribute|
|
30
|
+
validators.each do |validator_key, args|
|
31
|
+
validator = ValidatorRegistry[validator_key]
|
32
|
+
validations[attribute] << ValidatorEntry.new(validator, args)
|
33
|
+
end
|
34
|
+
validations[attribute] << ValidatorEntry.new(BlockValidator, block) if block
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module API
|
40
|
+
def validate(object, validation_result=ValidationResult.new, *args)
|
41
|
+
validations.each do |attribute, validators|
|
42
|
+
attribute_result = if attribute == :base
|
43
|
+
validation_result
|
44
|
+
else
|
45
|
+
ValidationResult.new
|
46
|
+
end
|
47
|
+
|
48
|
+
validators.each do |validator_entry|
|
49
|
+
validator = validator_entry.validator
|
50
|
+
if attribute == :base
|
51
|
+
validator.validate(object, attribute_result, *validator_entry.args)
|
52
|
+
elsif object
|
53
|
+
validator.validate(object.send(attribute), attribute_result, *validator_entry.args)
|
54
|
+
else
|
55
|
+
# no-op if we 're asked to validate an attribute for a nil value - that needs to be caught by a user defined `presence` validation instead
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
validation_result.merge_child!(attribute, attribute_result) unless attribute == :base
|
60
|
+
end
|
61
|
+
validation_result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class BlockValidator
|
66
|
+
class Context
|
67
|
+
include Subvalid::Validator::DSL
|
68
|
+
include Subvalid::Validator::API
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
72
|
+
#return unless object # don't pass nil object into block - this should be handled with a PresenceValidator if it needs to be flagged as a validation error
|
73
|
+
block = args[0]
|
74
|
+
|
75
|
+
context = Context.new
|
76
|
+
context.instance_exec(&block)
|
77
|
+
context.validate(object, validation_result, args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Subvalid
|
2
|
+
class ValidatorRegistry
|
3
|
+
class << self
|
4
|
+
def [](key)
|
5
|
+
validators[key] or raise "no validator with key=#{key}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def register(key, validator)
|
9
|
+
validators[key] = validator
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def validators
|
14
|
+
@validators ||= {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class FormatValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
options = args.to_h rescue args
|
6
|
+
with = nil
|
7
|
+
message = "is invalid"
|
8
|
+
case options
|
9
|
+
when Regexp
|
10
|
+
with = options
|
11
|
+
when Hash
|
12
|
+
with = options.fetch(:with)
|
13
|
+
message = options[:message] || message
|
14
|
+
else
|
15
|
+
raise "don't know what to do with #{options}"
|
16
|
+
end
|
17
|
+
validation_result.add_error(message) unless with.match(object)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
ValidatorRegistry.register(:format, FormatValidator)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class InValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
options = args.to_h rescue args
|
6
|
+
within = nil
|
7
|
+
message = "is not included in the list"
|
8
|
+
case options
|
9
|
+
when Hash
|
10
|
+
within = options.fetch(:within)
|
11
|
+
message = options[:message] || message
|
12
|
+
when Array
|
13
|
+
within = options
|
14
|
+
else
|
15
|
+
raise "don't know what to do with #{options}"
|
16
|
+
end
|
17
|
+
validation_result.add_error(message) unless within.include?(object)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
ValidatorRegistry.register(:in, InValidator)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class LengthValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
return unless object
|
6
|
+
args = args.to_h
|
7
|
+
args.each do |operator, value|
|
8
|
+
case operator
|
9
|
+
when :maximum
|
10
|
+
validation_result.add_error("is too long, maximum is #{value}") if object.size > value
|
11
|
+
# TODO ALL the other operators from http://guides.rubyonrails.org/active_record_validations.html#length
|
12
|
+
else
|
13
|
+
raise "don't know what to do with operator=#{operator}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
ValidatorRegistry.register(:length, LengthValidator)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class NumericalityValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
args = args.to_h
|
6
|
+
args.each do |operator, value|
|
7
|
+
case operator
|
8
|
+
when :greater_than_or_equal_to
|
9
|
+
validation_result.add_error("must be greater than or equal to #{value}" ) unless object >= value
|
10
|
+
# TODO ALL the other operators from http://guides.rubyonrails.org/active_record_validations.html#numericality
|
11
|
+
else
|
12
|
+
raise "don't know what to do with operator=#{operator}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
ValidatorRegistry.register(:numericality, NumericalityValidator)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class PresenceValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
present = if object
|
6
|
+
if object.respond_to?(:present?)
|
7
|
+
object.present?
|
8
|
+
else
|
9
|
+
object
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
validation_result.add_error("is not present") unless present
|
14
|
+
end
|
15
|
+
end
|
16
|
+
ValidatorRegistry.register(:presence, PresenceValidator)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Subvalid
|
2
|
+
module Validators
|
3
|
+
class WithValidator
|
4
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
5
|
+
case args[0]
|
6
|
+
when Class
|
7
|
+
klass = args[0]
|
8
|
+
klass.validate(object, validation_result)
|
9
|
+
when Proc
|
10
|
+
prok = args[0]
|
11
|
+
prok.(object, validation_result)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
ValidatorRegistry.register(:with, WithValidator)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "subvalid/validators/format_validator"
|
2
|
+
require "subvalid/validators/in_validator"
|
3
|
+
require "subvalid/validators/length_validator"
|
4
|
+
require "subvalid/validators/numericality_validator"
|
5
|
+
require "subvalid/validators/presence_validator"
|
6
|
+
require "subvalid/validators/with_validator"
|
7
|
+
module Subvalid
|
8
|
+
module Validators
|
9
|
+
end
|
10
|
+
end
|
data/lib/subvalid.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subvalid::ValidationResult do
|
4
|
+
describe "#valid?" do
|
5
|
+
context "when there are no errors" do
|
6
|
+
context "when children have no errors" do
|
7
|
+
before do
|
8
|
+
child = Subvalid::ValidationResult.new
|
9
|
+
subject.children[:foo] = child
|
10
|
+
end
|
11
|
+
it { is_expected.to be_valid }
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when children have errors" do
|
15
|
+
before do
|
16
|
+
child = Subvalid::ValidationResult.new
|
17
|
+
child.add_error("this is an error")
|
18
|
+
subject.children[:foo] = child
|
19
|
+
end
|
20
|
+
it { is_expected.to_not be_valid }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when there are errors" do
|
25
|
+
before do
|
26
|
+
subject.add_error("this is an error")
|
27
|
+
end
|
28
|
+
it { is_expected.to_not be_valid }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#add_error" do
|
33
|
+
it "adds the error" do
|
34
|
+
subject.add_error("duh")
|
35
|
+
subject.add_error("doh")
|
36
|
+
expect(subject.errors).to eq(["duh", "doh"])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#[]" do
|
41
|
+
context "when child attribute exists" do
|
42
|
+
let(:child) { Subvalid::ValidationResult.new }
|
43
|
+
before do
|
44
|
+
subject.children[:foo] = child
|
45
|
+
end
|
46
|
+
it "returns the child validation result" do
|
47
|
+
expect(subject[:foo]).to be(child)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when child attribute does not exist" do
|
52
|
+
it "returns nil" do
|
53
|
+
expect(subject[:blah]).to be_nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "merge_child!" do
|
59
|
+
context "when child attribute already exists" do
|
60
|
+
let(:child1) {
|
61
|
+
result = Subvalid::ValidationResult.new
|
62
|
+
result.add_error("Insufficient cheese")
|
63
|
+
grandchild = Subvalid::ValidationResult.new
|
64
|
+
grandchild.add_error("not tasty")
|
65
|
+
result.merge_child!(:tastiness, grandchild)
|
66
|
+
grandchild2 = Subvalid::ValidationResult.new
|
67
|
+
grandchild2.add_error("it's not OK")
|
68
|
+
result.merge_child!(:ok, grandchild2)
|
69
|
+
result
|
70
|
+
}
|
71
|
+
let(:child2) {
|
72
|
+
result = Subvalid::ValidationResult.new
|
73
|
+
result.add_error("blue cheese is awful")
|
74
|
+
grandchild = Subvalid::ValidationResult.new
|
75
|
+
grandchild.add_error("more smelly than tasty")
|
76
|
+
result.merge_child!(:tastiness, grandchild)
|
77
|
+
result
|
78
|
+
}
|
79
|
+
it "merges results together recursively" do
|
80
|
+
subject.merge_child!(:cheese, child1)
|
81
|
+
subject.merge_child!(:cheese, child2)
|
82
|
+
expect(subject[:cheese].errors).to eq(["Insufficient cheese", "blue cheese is awful"])
|
83
|
+
expect(subject[:cheese][:tastiness].errors).to eq(["not tasty", "more smelly than tasty"])
|
84
|
+
expect(subject[:cheese][:ok].errors).to eq(["it's not OK"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when child attribute doesn't exist" do
|
89
|
+
let(:child) {
|
90
|
+
result = Subvalid::ValidationResult.new
|
91
|
+
result.add_error("Insufficient cheese")
|
92
|
+
result
|
93
|
+
}
|
94
|
+
it "sets the child result from the passed in result" do
|
95
|
+
subject.merge_child!(:cheese, child)
|
96
|
+
expect(subject[:cheese].errors).to eq(["Insufficient cheese"])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#to_h" do
|
102
|
+
before do
|
103
|
+
child = Subvalid::ValidationResult.new
|
104
|
+
child.add_error("this is an error")
|
105
|
+
grandchild1 = Subvalid::ValidationResult.new
|
106
|
+
grandchild1.add_error("this is another error")
|
107
|
+
grandchild2 = Subvalid::ValidationResult.new # no errors
|
108
|
+
child.children[:bar] = grandchild1
|
109
|
+
child.children[:baz] = grandchild2
|
110
|
+
subject.children[:foo] = child
|
111
|
+
subject.add_error("this is a base error")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "generates a hash of attributes with errors" do
|
115
|
+
expect(subject.to_h).to eq({
|
116
|
+
errors: ["this is a base error"],
|
117
|
+
foo: {
|
118
|
+
errors: ["this is an error"],
|
119
|
+
bar: {
|
120
|
+
errors: ["this is another error"]
|
121
|
+
}
|
122
|
+
}
|
123
|
+
})
|
124
|
+
end
|
125
|
+
|
126
|
+
it "has errors on attribute itself in special :errors key" do
|
127
|
+
hash = subject.to_h
|
128
|
+
expect(hash[:errors]).to eq(["this is a base error"])
|
129
|
+
expect(hash[:foo][:errors]).to eq(["this is an error"])
|
130
|
+
expect(hash[:foo][:bar][:errors]).to eq(["this is another error"])
|
131
|
+
end
|
132
|
+
|
133
|
+
it "doesn't include anything for attributes that are valid" do
|
134
|
+
expect(subject.to_h[:foo]).to_not have_key(:baz)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#flatten" do
|
139
|
+
before do
|
140
|
+
child = Subvalid::ValidationResult.new
|
141
|
+
child.add_error("this is an error")
|
142
|
+
grandchild1 = Subvalid::ValidationResult.new
|
143
|
+
grandchild1.add_error("this is another error")
|
144
|
+
grandchild2 = Subvalid::ValidationResult.new # no errors
|
145
|
+
child.children[:bar] = grandchild1
|
146
|
+
child.children[:baz] = grandchild2
|
147
|
+
subject.children[:foo] = child
|
148
|
+
subject.add_error("this is a base error")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "generates flat list of error messages recursively" do
|
152
|
+
expect(subject.flatten).to eq([
|
153
|
+
"this is a base error",
|
154
|
+
"foo: this is an error",
|
155
|
+
"foo, bar: this is another error"
|
156
|
+
])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subvalid::ValidatorRegistry do
|
4
|
+
describe "#[]" do
|
5
|
+
before do
|
6
|
+
described_class.register(:accessor_test, "a validator")
|
7
|
+
end
|
8
|
+
context "when validator with key is registered" do
|
9
|
+
it "returns the validator" do
|
10
|
+
expect(described_class[:accessor_test]).to eq("a validator")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when validator doesn't exist" do
|
15
|
+
it "raises an error" do
|
16
|
+
expect { described_class[:bad_key] }.to raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#register" do
|
22
|
+
it "add the validator with the key" do
|
23
|
+
# yes, this is a duplicat of the #[] spec, but it demonstrates the publicly API, so we'll have it
|
24
|
+
described_class.register(:register_test, "registered validator")
|
25
|
+
expect(described_class[:register_test]).to eq("registered validator")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subvalid::Validator do
|
4
|
+
let(:stub_validator) {
|
5
|
+
-> (object, result) { "testing #{object} " }
|
6
|
+
}
|
7
|
+
class FooValidator
|
8
|
+
include Subvalid::Validator
|
9
|
+
|
10
|
+
STUB_VALIDATOR = -> (object, result) { result.add_error("testing #{object}") }
|
11
|
+
|
12
|
+
|
13
|
+
validates with: STUB_VALIDATOR
|
14
|
+
validates :foo, with: STUB_VALIDATOR
|
15
|
+
validates :child do
|
16
|
+
validates :boz, with: STUB_VALIDATOR
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class TestValidator
|
21
|
+
def self.validate(object, validation_result=ValidationResult.new, *args)
|
22
|
+
validation_result.add_error("testing #{object}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
Subvalid::ValidatorRegistry.register(:test, TestValidator)
|
26
|
+
|
27
|
+
Poro = Struct.new(:foo, :bar, :child) do
|
28
|
+
def to_s
|
29
|
+
"I'M A PORO"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
PoroChild = Struct.new(:baz, :boz) do
|
34
|
+
def to_s
|
35
|
+
"I'M A PORO CHILD"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:poro) { Poro.new("foo", "bar",
|
40
|
+
PoroChild.new("baz", "boz")) }
|
41
|
+
|
42
|
+
describe "#validate" do
|
43
|
+
subject { FooValidator.validate(poro) }
|
44
|
+
it "returns a validation result" do
|
45
|
+
expect(subject).to be_a(Subvalid::ValidationResult)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "validates base object" do
|
49
|
+
expect(subject.errors).to eq(["testing I'M A PORO"])
|
50
|
+
end
|
51
|
+
|
52
|
+
it "validates attribute on object" do
|
53
|
+
expect(subject[:foo].errors).to eq(["testing foo"])
|
54
|
+
end
|
55
|
+
|
56
|
+
it "validates child" do
|
57
|
+
expect(subject[:child][:boz].errors).to eq(["testing boz"])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/subvalid.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "subvalid/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "subvalid"
|
8
|
+
spec.version = Subvalid::VERSION
|
9
|
+
spec.authors = ["Julian Doherty"]
|
10
|
+
spec.email = ["madlep@madlep.com"]
|
11
|
+
spec.summary = %q{Subjective validation for Plain Old Ruby Objects}
|
12
|
+
spec.description = %q{Subvalid allows you to use a familiar syntax to define validator classes to validate plain objects. Rather than hard coding single validation logic into the objects themselves, different validation logic can be used depending on the context.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "pry"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: subvalid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julian Doherty
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Subvalid allows you to use a familiar syntax to define validator classes
|
70
|
+
to validate plain objects. Rather than hard coding single validation logic into
|
71
|
+
the objects themselves, different validation logic can be used depending on the
|
72
|
+
context.
|
73
|
+
email:
|
74
|
+
- madlep@madlep.com
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".gitignore"
|
80
|
+
- ".rspec"
|
81
|
+
- ".travis.yml"
|
82
|
+
- Gemfile
|
83
|
+
- LICENSE.txt
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- lib/subvalid.rb
|
87
|
+
- lib/subvalid/validation_result.rb
|
88
|
+
- lib/subvalid/validator.rb
|
89
|
+
- lib/subvalid/validator_registry.rb
|
90
|
+
- lib/subvalid/validators.rb
|
91
|
+
- lib/subvalid/validators/format_validator.rb
|
92
|
+
- lib/subvalid/validators/in_validator.rb
|
93
|
+
- lib/subvalid/validators/length_validator.rb
|
94
|
+
- lib/subvalid/validators/numericality_validator.rb
|
95
|
+
- lib/subvalid/validators/presence_validator.rb
|
96
|
+
- lib/subvalid/validators/with_validator.rb
|
97
|
+
- lib/subvalid/version.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
- spec/subvalid/validation_result_spec.rb
|
100
|
+
- spec/subvalid/validator_registry_spec.rb
|
101
|
+
- spec/subvalid/validator_spec.rb
|
102
|
+
- spec/subvalid_spec.rb
|
103
|
+
- subvalid.gemspec
|
104
|
+
homepage: ''
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.4.5
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Subjective validation for Plain Old Ruby Objects
|
128
|
+
test_files:
|
129
|
+
- spec/spec_helper.rb
|
130
|
+
- spec/subvalid/validation_result_spec.rb
|
131
|
+
- spec/subvalid/validator_registry_spec.rb
|
132
|
+
- spec/subvalid/validator_spec.rb
|
133
|
+
- spec/subvalid_spec.rb
|