dry-validation 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.
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