dry-validation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/.travis.yml +29 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE +20 -0
  10. data/README.md +297 -0
  11. data/Rakefile +12 -0
  12. data/config/errors.yml +35 -0
  13. data/dry-validation.gemspec +25 -0
  14. data/examples/basic.rb +21 -0
  15. data/examples/nested.rb +30 -0
  16. data/examples/rule_ast.rb +33 -0
  17. data/lib/dry-validation.rb +1 -0
  18. data/lib/dry/validation.rb +12 -0
  19. data/lib/dry/validation/error.rb +43 -0
  20. data/lib/dry/validation/error_compiler.rb +116 -0
  21. data/lib/dry/validation/messages.rb +71 -0
  22. data/lib/dry/validation/predicate.rb +39 -0
  23. data/lib/dry/validation/predicate_set.rb +22 -0
  24. data/lib/dry/validation/predicates.rb +88 -0
  25. data/lib/dry/validation/result.rb +64 -0
  26. data/lib/dry/validation/rule.rb +125 -0
  27. data/lib/dry/validation/rule_compiler.rb +57 -0
  28. data/lib/dry/validation/schema.rb +74 -0
  29. data/lib/dry/validation/schema/definition.rb +15 -0
  30. data/lib/dry/validation/schema/key.rb +39 -0
  31. data/lib/dry/validation/schema/rule.rb +28 -0
  32. data/lib/dry/validation/schema/value.rb +31 -0
  33. data/lib/dry/validation/version.rb +5 -0
  34. data/rakelib/rubocop.rake +18 -0
  35. data/spec/fixtures/errors.yml +4 -0
  36. data/spec/integration/custom_error_messages_spec.rb +35 -0
  37. data/spec/integration/custom_predicates_spec.rb +57 -0
  38. data/spec/integration/validation_spec.rb +118 -0
  39. data/spec/shared/predicates.rb +31 -0
  40. data/spec/spec_helper.rb +18 -0
  41. data/spec/unit/error_compiler_spec.rb +165 -0
  42. data/spec/unit/predicate_spec.rb +37 -0
  43. data/spec/unit/predicates/empty_spec.rb +38 -0
  44. data/spec/unit/predicates/eql_spec.rb +21 -0
  45. data/spec/unit/predicates/exclusion_spec.rb +35 -0
  46. data/spec/unit/predicates/filled_spec.rb +38 -0
  47. data/spec/unit/predicates/format_spec.rb +21 -0
  48. data/spec/unit/predicates/gt_spec.rb +40 -0
  49. data/spec/unit/predicates/gteq_spec.rb +40 -0
  50. data/spec/unit/predicates/inclusion_spec.rb +35 -0
  51. data/spec/unit/predicates/int_spec.rb +34 -0
  52. data/spec/unit/predicates/key_spec.rb +29 -0
  53. data/spec/unit/predicates/lt_spec.rb +40 -0
  54. data/spec/unit/predicates/lteq_spec.rb +40 -0
  55. data/spec/unit/predicates/max_size_spec.rb +49 -0
  56. data/spec/unit/predicates/min_size_spec.rb +49 -0
  57. data/spec/unit/predicates/nil_spec.rb +28 -0
  58. data/spec/unit/predicates/size_spec.rb +49 -0
  59. data/spec/unit/predicates/str_spec.rb +32 -0
  60. data/spec/unit/rule/each_spec.rb +20 -0
  61. data/spec/unit/rule/key_spec.rb +27 -0
  62. data/spec/unit/rule/set_spec.rb +32 -0
  63. data/spec/unit/rule/value_spec.rb +42 -0
  64. data/spec/unit/rule_compiler_spec.rb +86 -0
  65. metadata +230 -0
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/dry/validation/version', __FILE__)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'dry-validation'
6
+ spec.version = Dry::Validation::VERSION
7
+ spec.authors = ['Andy Holland', 'Piotr Solnica']
8
+ spec.email = ['andyholland1991@aol.com', 'piotr.solnica@gmail.com']
9
+ spec.summary = 'A simple validation library'
10
+ spec.homepage = 'https://github.com/dryrb/dry-validation'
11
+ spec.license = 'MIT'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_runtime_dependency 'dry-configurable', '~> 0.1'
19
+ spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.6'
20
+ spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec'
25
+ end
@@ -0,0 +1,21 @@
1
+ require 'dry-validation'
2
+
3
+ class Schema < Dry::Validation::Schema
4
+ key(:email) { |email| email.filled? }
5
+
6
+ key(:age) do |age|
7
+ age.int? & age.gt?(18)
8
+ end
9
+ end
10
+
11
+ schema = Schema.new
12
+
13
+ errors = schema.messages(email: 'jane@doe.org', age: 19)
14
+
15
+ puts errors.inspect
16
+ # []
17
+
18
+ errors = schema.messages(email: nil, age: 19)
19
+
20
+ puts errors.inspect
21
+ # [[:email, ["email must be filled"]]]
@@ -0,0 +1,30 @@
1
+ require 'dry-validation'
2
+
3
+ class Schema < Dry::Validation::Schema
4
+ key(:address) do |address|
5
+ address.key(:city) do |city|
6
+ city.min_size?(3)
7
+ end
8
+
9
+ address.key(:street) do |street|
10
+ street.filled?
11
+ end
12
+
13
+ address.key(:country) do |country|
14
+ country.key(:name, &:filled?)
15
+ country.key(:code, &:filled?)
16
+ end
17
+ end
18
+ end
19
+
20
+ schema = Schema.new
21
+
22
+ errors = schema.messages({})
23
+
24
+ puts errors.inspect
25
+ #<Dry::Validation::Error::Set:0x007fc4f89c4360 @errors=[#<Dry::Validation::Error:0x007fc4f89c4108 @result=#<Dry::Validation::Result::Value success?=false input=nil rule=#<Dry::Validation::Rule::Key name=:address predicate=#<Dry::Validation::Predicate id=:key?>>>>]>
26
+
27
+ errors = schema.messages(address: { city: 'NYC' })
28
+
29
+ puts errors.inspect
30
+ #<Dry::Validation::Error::Set:0x007fd151189b18 @errors=[#<Dry::Validation::Error:0x007fd151188e20 @result=#<Dry::Validation::Result::Set success?=false input={:city=>"NYC"} rule=#<Dry::Validation::Rule::Set name=:address predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:city predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:city predicate=#<Dry::Validation::Predicate id=:min_size?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:street predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:street predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:country predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Set name=:country predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:name predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:name predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:code predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:code predicate=#<Dry::Validation::Predicate id=:filled?>>>]>>]>>>]>
@@ -0,0 +1,33 @@
1
+ require 'dry-validation'
2
+
3
+ ast = [
4
+ [
5
+ :and,
6
+ [
7
+ [:key, [:age, [:predicate, [:key?, []]]]],
8
+ [
9
+ :and,
10
+ [
11
+ [:val, [:age, [:predicate, [:filled?, []]]]],
12
+ [:val, [:age, [:predicate, [:gt?, [18]]]]]
13
+ ]
14
+ ]
15
+ ]
16
+ ]
17
+ ]
18
+
19
+ compiler = Dry::Validation::RuleCompiler.new(Dry::Validation::Predicates)
20
+
21
+ rules = compiler.call(ast)
22
+
23
+ puts rules.inspect
24
+ # [
25
+ # #<Dry::Validation::Rule::Conjunction
26
+ # left=#<Dry::Validation::Rule::Key name=:age predicate=#<Dry::Validation::Predicate id=:key?>>
27
+ # right=#<Dry::Validation::Rule::Conjunction
28
+ # left=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:filled?>>
29
+ # right=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:gt?>>>>
30
+ # ]
31
+
32
+ puts rules.map(&:to_ary).inspect
33
+ # [[:and, [:key, [:age, [:predicate, [:key?, [:age]]]]], [[:and, [:val, [:age, [:predicate, [:filled?, []]]]], [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]]]
@@ -0,0 +1 @@
1
+ require 'dry/validation'
@@ -0,0 +1,12 @@
1
+ require 'dry-equalizer'
2
+ require 'dry-configurable'
3
+ require 'dry-container'
4
+
5
+ # A collection of micro-libraries, each intended to encapsulate
6
+ # a common task in Ruby
7
+ module Dry
8
+ module Validation
9
+ end
10
+ end
11
+
12
+ require 'dry/validation/schema'
@@ -0,0 +1,43 @@
1
+ module Dry
2
+ module Validation
3
+ class Error
4
+ class Set
5
+ include Enumerable
6
+
7
+ attr_reader :errors
8
+
9
+ def initialize
10
+ @errors = []
11
+ end
12
+
13
+ def each(&block)
14
+ errors.each(&block)
15
+ end
16
+
17
+ def empty?
18
+ errors.empty?
19
+ end
20
+
21
+ def <<(error)
22
+ errors << error
23
+ end
24
+
25
+ def to_ary
26
+ errors.map { |error| error.to_ary }
27
+ end
28
+ alias_method :to_a, :to_ary
29
+ end
30
+
31
+ attr_reader :result
32
+
33
+ def initialize(result)
34
+ @result = result
35
+ end
36
+
37
+ def to_ary
38
+ [:error, result.to_ary]
39
+ end
40
+ alias_method :to_a, :to_ary
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,116 @@
1
+ module Dry
2
+ module Validation
3
+ class ErrorCompiler
4
+ attr_reader :messages
5
+
6
+ def initialize(messages)
7
+ @messages = messages
8
+ end
9
+
10
+ def call(ast)
11
+ ast.map { |node| visit(node) }
12
+ end
13
+
14
+ def visit(node, *args)
15
+ __send__(:"visit_#{node[0]}", node[1], *args)
16
+ end
17
+
18
+ def visit_error(error)
19
+ visit(error)
20
+ end
21
+
22
+ def visit_input(input, *args)
23
+ name, value, rules = input
24
+ [name, rules.map { |rule| visit(rule, name, value) }]
25
+ end
26
+
27
+ def visit_key(rule, name, value)
28
+ _, predicate = rule
29
+ visit(predicate, value, name)
30
+ end
31
+
32
+ def visit_val(rule, name, value)
33
+ name, predicate = rule
34
+ visit(predicate, value, name)
35
+ end
36
+
37
+ def visit_predicate(predicate, value, name)
38
+ messages.lookup(predicate[0], name, predicate[1][0]) % visit(predicate, value).merge(name: name)
39
+ end
40
+
41
+ def visit_key?(*args, value)
42
+ { name: args[0][0] }
43
+ end
44
+
45
+ def visit_empty?(*args, value)
46
+ { value: value }
47
+ end
48
+
49
+ def visit_exclusion?(*args, value)
50
+ { list: args[0][0].join(', ') }
51
+ end
52
+
53
+ def visit_inclusion?(*args, value)
54
+ { list: args[0][0].join(', ') }
55
+ end
56
+
57
+ def visit_gt?(*args, value)
58
+ { num: args[0][0], value: value }
59
+ end
60
+
61
+ def visit_gteq?(*args, value)
62
+ { num: args[0][0], value: value }
63
+ end
64
+
65
+ def visit_lt?(*args, value)
66
+ { num: args[0][0], value: value }
67
+ end
68
+
69
+ def visit_lteq?(*args, value)
70
+ { num: args[0][0], value: value }
71
+ end
72
+
73
+ def visit_int?(*args, value)
74
+ { num: args[0][0], value: value }
75
+ end
76
+
77
+ def visit_max_size?(*args, value)
78
+ { num: args[0][0], value: value }
79
+ end
80
+
81
+ def visit_min_size?(*args, value)
82
+ { num: args[0][0], value: value }
83
+ end
84
+
85
+ def visit_eql?(*args, value)
86
+ { eql_value: args[0][0], value: value }
87
+ end
88
+
89
+ def visit_size?(*args, value)
90
+ num = args[0][0]
91
+
92
+ if num.is_a?(Range)
93
+ { left: num.first, right: num.last, value: value }
94
+ else
95
+ { num: args[0][0], value: value }
96
+ end
97
+ end
98
+
99
+ def visit_str?(*args, value)
100
+ { value: value }
101
+ end
102
+
103
+ def visit_format?(*args, value)
104
+ {}
105
+ end
106
+
107
+ def visit_nil?(*args, value)
108
+ {}
109
+ end
110
+
111
+ def visit_filled?(*args)
112
+ {}
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,71 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module Dry
5
+ module Validation
6
+ class Messages
7
+ DEFAULT_PATH = Pathname(__dir__).join('../../../config/errors.yml').freeze
8
+
9
+ attr_reader :data
10
+
11
+ def self.default
12
+ load(DEFAULT_PATH)
13
+ end
14
+
15
+ def self.load(path)
16
+ new(load_yaml(path))
17
+ end
18
+
19
+ def self.load_yaml(path)
20
+ symbolize_keys(YAML.load_file(path))
21
+ end
22
+
23
+ def self.symbolize_keys(hash)
24
+ hash.each_with_object({}) do |(k, v), r|
25
+ r[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
26
+ end
27
+ end
28
+
29
+ class Namespaced
30
+ attr_reader :namespace, :fallback
31
+
32
+ def initialize(namespace, fallback)
33
+ @namespace = namespace
34
+ @fallback = fallback
35
+ end
36
+
37
+ def lookup(*args)
38
+ namespace.lookup(*args) { fallback.lookup(*args) }
39
+ end
40
+ end
41
+
42
+ def initialize(data)
43
+ @data = data
44
+ end
45
+
46
+ def merge(overrides)
47
+ if overrides.is_a?(Hash)
48
+ self.class.new(data.merge(overrides))
49
+ else
50
+ self.class.new(data.merge(Messages.load_yaml(overrides)))
51
+ end
52
+ end
53
+
54
+ def namespaced(namespace)
55
+ Namespaced.new(Messages.new(data[namespace]), self)
56
+ end
57
+
58
+ def lookup(identifier, key, arg, &block)
59
+ message = data.fetch(:attributes, {}).fetch(key, {}).fetch(identifier) do
60
+ data.fetch(identifier, &block)
61
+ end
62
+
63
+ if message.is_a?(Hash)
64
+ message.fetch(arg.class.name.downcase.to_sym, message.fetch(:default))
65
+ else
66
+ message
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ module Dry
2
+ module Validation
3
+ def self.Predicate(block)
4
+ case block
5
+ when Method then Predicate.new(block.name, &block)
6
+ else raise ArgumentError, 'predicate needs an :id'
7
+ end
8
+ end
9
+
10
+ class Predicate
11
+ include Dry::Equalizer(:id)
12
+
13
+ attr_reader :id, :args, :fn
14
+
15
+ def initialize(id, *args, &block)
16
+ @id = id
17
+ @fn = block
18
+ @args = args
19
+ end
20
+
21
+ def call(*args)
22
+ fn.(*args)
23
+ end
24
+
25
+ def negation
26
+ self.class.new(:"not_#{id}") { |input| !fn.(input) }
27
+ end
28
+
29
+ def curry(*args)
30
+ self.class.new(id, *args, &fn.curry.(*args))
31
+ end
32
+
33
+ def to_ary
34
+ [:predicate, [id, args]]
35
+ end
36
+ alias_method :to_a, :to_ary
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ require 'dry/validation/predicate'
2
+
3
+ module Dry
4
+ module Validation
5
+ module PredicateSet
6
+ module Methods
7
+ def predicate(name, &block)
8
+ register(name) { Predicate.new(name, &block) }
9
+ end
10
+
11
+ def import(predicate_set)
12
+ merge(predicate_set)
13
+ end
14
+ end
15
+
16
+ def self.extended(other)
17
+ super
18
+ other.extend(Methods, Dry::Container::Mixin)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,88 @@
1
+ require 'dry/validation/predicate_set'
2
+
3
+ module Dry
4
+ module Validation
5
+ module Predicates
6
+ extend PredicateSet
7
+
8
+ def self.included(other)
9
+ super
10
+ other.extend(PredicateSet)
11
+ other.import(self)
12
+ end
13
+
14
+ predicate(:nil?) do |input|
15
+ input.nil?
16
+ end
17
+
18
+ predicate(:key?) do |name, input|
19
+ input.key?(name)
20
+ end
21
+
22
+ predicate(:empty?) do |input|
23
+ case input
24
+ when String, Array, Hash then input.empty?
25
+ when nil then true
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ predicate(:filled?) do |input|
32
+ !self[:empty?].(input)
33
+ end
34
+
35
+ predicate(:int?) do |input|
36
+ input.is_a?(Fixnum)
37
+ end
38
+
39
+ predicate(:str?) do |input|
40
+ input.is_a?(String)
41
+ end
42
+
43
+ predicate(:lt?) do |num, input|
44
+ input < num
45
+ end
46
+
47
+ predicate(:gt?) do |num, input|
48
+ input > num
49
+ end
50
+
51
+ predicate(:lteq?) do |num, input|
52
+ !self[:gt?].(num, input)
53
+ end
54
+
55
+ predicate(:gteq?) do |num, input|
56
+ !self[:lt?].(num, input)
57
+ end
58
+
59
+ predicate(:size?) do |num, input|
60
+ input.size == num
61
+ end
62
+
63
+ predicate(:min_size?) do |num, input|
64
+ input.size >= num
65
+ end
66
+
67
+ predicate(:max_size?) do |num, input|
68
+ input.size <= num
69
+ end
70
+
71
+ predicate(:inclusion?) do |list, input|
72
+ list.include?(input)
73
+ end
74
+
75
+ predicate(:exclusion?) do |list, input|
76
+ !self[:inclusion?].(list, input)
77
+ end
78
+
79
+ predicate(:eql?) do |left, right|
80
+ left.eql?(right)
81
+ end
82
+
83
+ predicate(:format?) do |regex, input|
84
+ !regex.match(input).nil?
85
+ end
86
+ end
87
+ end
88
+ end