dry-validation 0.6.0 → 0.7.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG.md +42 -0
  5. data/Gemfile +8 -1
  6. data/README.md +13 -89
  7. data/config/errors.yml +35 -29
  8. data/dry-validation.gemspec +2 -2
  9. data/examples/basic.rb +3 -7
  10. data/examples/each.rb +3 -8
  11. data/examples/form.rb +3 -6
  12. data/examples/nested.rb +7 -15
  13. data/lib/dry/validation.rb +33 -5
  14. data/lib/dry/validation/error.rb +10 -26
  15. data/lib/dry/validation/error_compiler.rb +69 -99
  16. data/lib/dry/validation/error_compiler/input.rb +148 -0
  17. data/lib/dry/validation/hint_compiler.rb +83 -33
  18. data/lib/dry/validation/input_processor_compiler.rb +98 -0
  19. data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
  20. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
  21. data/lib/dry/validation/messages/abstract.rb +30 -10
  22. data/lib/dry/validation/messages/i18n.rb +2 -1
  23. data/lib/dry/validation/messages/namespaced.rb +1 -0
  24. data/lib/dry/validation/messages/yaml.rb +8 -5
  25. data/lib/dry/validation/result.rb +33 -25
  26. data/lib/dry/validation/schema.rb +168 -61
  27. data/lib/dry/validation/schema/attr.rb +5 -27
  28. data/lib/dry/validation/schema/check.rb +24 -0
  29. data/lib/dry/validation/schema/dsl.rb +97 -0
  30. data/lib/dry/validation/schema/form.rb +2 -26
  31. data/lib/dry/validation/schema/key.rb +32 -28
  32. data/lib/dry/validation/schema/rule.rb +88 -32
  33. data/lib/dry/validation/schema/value.rb +77 -27
  34. data/lib/dry/validation/schema_compiler.rb +38 -0
  35. data/lib/dry/validation/version.rb +1 -1
  36. data/spec/fixtures/locales/pl.yml +1 -1
  37. data/spec/integration/attr_spec.rb +122 -0
  38. data/spec/integration/custom_error_messages_spec.rb +9 -11
  39. data/spec/integration/custom_predicates_spec.rb +68 -18
  40. data/spec/integration/error_compiler_spec.rb +259 -65
  41. data/spec/integration/hints_spec.rb +28 -9
  42. data/spec/integration/injecting_rules_spec.rb +11 -12
  43. data/spec/integration/localized_error_messages_spec.rb +16 -16
  44. data/spec/integration/messages/i18n_spec.rb +9 -5
  45. data/spec/integration/optional_keys_spec.rb +9 -11
  46. data/spec/integration/schema/array_schema_spec.rb +23 -0
  47. data/spec/integration/schema/check_rules_spec.rb +39 -31
  48. data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
  49. data/spec/integration/schema/each_with_set_spec.rb +23 -24
  50. data/spec/integration/schema/form_spec.rb +122 -0
  51. data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
  52. data/spec/integration/schema/input_processor_spec.rb +46 -0
  53. data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
  54. data/spec/integration/schema/macros/maybe_spec.rb +32 -0
  55. data/spec/integration/schema/macros/required_spec.rb +59 -0
  56. data/spec/integration/schema/macros/when_spec.rb +65 -0
  57. data/spec/integration/schema/nested_values_spec.rb +41 -0
  58. data/spec/integration/schema/not_spec.rb +14 -14
  59. data/spec/integration/schema/option_with_default_spec.rb +30 -0
  60. data/spec/integration/schema/reusing_schema_spec.rb +33 -0
  61. data/spec/integration/schema/using_types_spec.rb +29 -0
  62. data/spec/integration/schema/xor_spec.rb +17 -14
  63. data/spec/integration/schema_spec.rb +75 -245
  64. data/spec/shared/rule_compiler.rb +8 -0
  65. data/spec/spec_helper.rb +13 -0
  66. data/spec/unit/hint_compiler_spec.rb +10 -10
  67. data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
  68. data/spec/unit/schema/key_spec.rb +33 -0
  69. data/spec/unit/schema/rule_spec.rb +7 -6
  70. data/spec/unit/schema/value_spec.rb +187 -54
  71. metadata +53 -31
  72. data/.rubocop.yml +0 -16
  73. data/.rubocop_todo.yml +0 -7
  74. data/lib/dry/validation/input_type_compiler.rb +0 -83
  75. data/lib/dry/validation/schema/definition.rb +0 -74
  76. data/lib/dry/validation/schema/result.rb +0 -68
  77. data/rakelib/rubocop.rake +0 -18
  78. data/spec/integration/rule_groups_spec.rb +0 -94
  79. data/spec/integration/schema/attrs_spec.rb +0 -38
  80. data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
  81. data/spec/integration/schema/grouped_rules_spec.rb +0 -57
  82. data/spec/integration/schema/nested_spec.rb +0 -31
  83. data/spec/integration/schema_form_spec.rb +0 -97
@@ -0,0 +1,98 @@
1
+ require 'dry/types'
2
+ require 'dry/types/compiler'
3
+
4
+ module Dry
5
+ module Validation
6
+ class InputProcessorCompiler
7
+ attr_reader :type_compiler
8
+
9
+ DEFAULT_TYPE_NODE = [[:type, 'string']].freeze
10
+
11
+ def initialize
12
+ @type_compiler = Dry::Types::Compiler.new(Dry::Types)
13
+ end
14
+
15
+ def call(ast)
16
+ type_compiler.(hash_node(schema_ast(ast)))
17
+ end
18
+
19
+ def schema_ast(ast)
20
+ ast.map { |node| visit(node) }
21
+ end
22
+
23
+ def visit(node, *args)
24
+ send(:"visit_#{node[0]}", node[1], *args)
25
+ end
26
+
27
+ def visit_schema(node, *args)
28
+ hash_node(node.input_processor_ast(identifier))
29
+ end
30
+
31
+ def visit_or(node, *args)
32
+ left, right = node
33
+ [:sum, [visit(left, *args), visit(right, *args)]]
34
+ end
35
+
36
+ def visit_and(node, first = true)
37
+ if first
38
+ name, type = node.map { |n| visit(n, false) }.uniq
39
+ [:key, [name, type]]
40
+ else
41
+ result = node.map { |n| visit(n, first) }.uniq
42
+
43
+ if result.size == 1
44
+ result.first
45
+ else
46
+ (result - self.class::DEFAULT_TYPE_NODE).first
47
+ end
48
+ end
49
+ end
50
+
51
+ def visit_implication(node)
52
+ key, types = node
53
+ [:key, [visit(key), visit(types, false)]]
54
+ end
55
+
56
+ def visit_key(node, *args)
57
+ _, other = node
58
+ visit(other, *args)
59
+ end
60
+
61
+ def visit_val(node, *args)
62
+ visit(node, *args)
63
+ end
64
+
65
+ def visit_set(node, *)
66
+ hash_node(node.map { |n| visit(n) })
67
+ end
68
+
69
+ def visit_each(node, *args)
70
+ array_node(visit(node, *args))
71
+ end
72
+
73
+ def visit_predicate(node, *args)
74
+ id, args = node
75
+
76
+ if id == :key?
77
+ args[0]
78
+ else
79
+ type(id, args)
80
+ end
81
+ end
82
+
83
+ def type(predicate, args)
84
+ default = self.class::PREDICATE_MAP[:default]
85
+
86
+ if predicate == :type?
87
+ const = args[0]
88
+ [:type, self.class::CONST_MAP[const] || Types.identifier(const)]
89
+ else
90
+ [:type, self.class::PREDICATE_MAP[predicate] || default]
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ require 'dry/validation/input_processor_compiler/sanitizer'
98
+ require 'dry/validation/input_processor_compiler/form'
@@ -0,0 +1,46 @@
1
+ module Dry
2
+ module Validation
3
+ class InputProcessorCompiler::Form < InputProcessorCompiler
4
+ PREDICATE_MAP = {
5
+ default: 'string',
6
+ none?: 'form.nil',
7
+ bool?: 'form.bool',
8
+ str?: 'string',
9
+ int?: 'form.int',
10
+ float?: 'form.float',
11
+ decimal?: 'form.decimal',
12
+ date?: 'form.date',
13
+ date_time?: 'form.date_time',
14
+ time?: 'form.time'
15
+ }.freeze
16
+
17
+ CONST_MAP = {
18
+ NilClass => 'form.nil',
19
+ String => 'string',
20
+ Fixnum => 'form.int',
21
+ Integer => 'form.int',
22
+ Float => 'form.float',
23
+ BigDecimal => 'form.decimal',
24
+ Array => 'form.array',
25
+ Hash => 'form.hash',
26
+ Date => 'form.date',
27
+ DateTime => 'form.date_time',
28
+ Time => 'form.time',
29
+ TrueClass => 'form.true',
30
+ FalseClass => 'form.false'
31
+ }.freeze
32
+
33
+ def identifier
34
+ :form
35
+ end
36
+
37
+ def hash_node(schema)
38
+ [:type, ['form.hash', [:symbolized, schema]]]
39
+ end
40
+
41
+ def array_node(members)
42
+ [:type, ['form.array', members]]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ module Dry
2
+ module Validation
3
+ class InputProcessorCompiler::Sanitizer < InputProcessorCompiler
4
+ PREDICATE_MAP = {
5
+ default: 'string',
6
+ none?: 'nil',
7
+ bool?: 'bool',
8
+ str?: 'string',
9
+ int?: 'int',
10
+ float?: 'float',
11
+ decimal?: 'decimal',
12
+ date?: 'date',
13
+ date_time?: 'date_time',
14
+ time?: 'time'
15
+ }.freeze
16
+
17
+ CONST_MAP = {
18
+ NilClass => 'nil',
19
+ String => 'string',
20
+ Fixnum => 'int',
21
+ Integer => 'int',
22
+ Float => 'float',
23
+ BigDecimal => 'decimal',
24
+ Array => 'array',
25
+ Hash => 'hash',
26
+ Date => 'date',
27
+ DateTime => 'date_time',
28
+ Time => 'time',
29
+ TrueClass => 'true',
30
+ FalseClass => 'false'
31
+ }.freeze
32
+
33
+ def identifier
34
+ :sanitizer
35
+ end
36
+
37
+ def hash_node(schema)
38
+ [:type, ['hash', [:schema, schema]]]
39
+ end
40
+
41
+ def array_node(members)
42
+ [:type, ['array', members]]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -5,9 +5,10 @@ module Dry
5
5
  module Validation
6
6
  module Messages
7
7
  class Abstract
8
- DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
9
-
10
8
  extend Dry::Configurable
9
+ include Dry::Equalizer(:config)
10
+
11
+ DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
11
12
 
12
13
  setting :paths, [DEFAULT_PATH]
13
14
  setting :root, 'errors'.freeze
@@ -34,12 +35,30 @@ module Dry
34
35
  String => 'string'
35
36
  )
36
37
 
38
+ def self.cache
39
+ @cache ||= ThreadSafe::Cache.new { |h, k| h[k] = ThreadSafe::Cache.new }
40
+ end
41
+
42
+ attr_reader :config
43
+
44
+ def initialize
45
+ @config = self.class.config
46
+ end
47
+
48
+ def rule(name, options = {})
49
+ path = "%{locale}.rules.#{name}"
50
+ get(path, options) if key?(path, options)
51
+ end
52
+
37
53
  def call(*args)
38
- cache.fetch_or_store(args.hash) { get(*lookup(*args)) }
54
+ cache.fetch_or_store(args.hash) do
55
+ path, opts = lookup(*args)
56
+ get(path, opts) if path
57
+ end
39
58
  end
40
59
  alias_method :[], :call
41
60
 
42
- def lookup(predicate, options)
61
+ def lookup(predicate, options = {})
43
62
  tokens = options.merge(
44
63
  root: root,
45
64
  predicate: predicate,
@@ -47,8 +66,13 @@ module Dry
47
66
  val_type: config.val_types[options[:val_type]]
48
67
  )
49
68
 
69
+ tokens[:rule] = predicate unless tokens.key?(:rule)
70
+
50
71
  opts = options.reject { |k, _| config.lookup_options.include?(k) }
51
- path = lookup_paths(tokens).detect { |key| key?(key, opts) && get(key).is_a?(String) }
72
+
73
+ path = lookup_paths(tokens).detect do |key|
74
+ key?(key, opts) && get(key, opts).is_a?(String)
75
+ end
52
76
 
53
77
  [path, opts]
54
78
  end
@@ -65,12 +89,8 @@ module Dry
65
89
  config.root
66
90
  end
67
91
 
68
- def config
69
- self.class.config
70
- end
71
-
72
92
  def cache
73
- @cache ||= ThreadSafe::Cache.new
93
+ self.class.cache[self]
74
94
  end
75
95
  end
76
96
  end
@@ -9,11 +9,12 @@ module Dry
9
9
  ::I18n.load_path.concat(config.paths)
10
10
 
11
11
  def initialize
12
+ super
12
13
  @t = I18n.method(:t)
13
14
  end
14
15
 
15
16
  def get(key, options = {})
16
- t.(key, options)
17
+ t.(key, options) if key
17
18
  end
18
19
 
19
20
  def key?(key, options)
@@ -5,6 +5,7 @@ module Dry
5
5
  attr_reader :namespace, :messages, :root
6
6
 
7
7
  def initialize(namespace, messages)
8
+ super()
8
9
  @namespace = namespace
9
10
  @messages = messages
10
11
  @root = messages.root
@@ -6,10 +6,12 @@ require 'dry/validation/messages/abstract'
6
6
  module Dry
7
7
  module Validation
8
8
  class Messages::YAML < Messages::Abstract
9
+ include Dry::Equalizer(:data)
10
+
9
11
  attr_reader :data
10
12
 
11
13
  configure do |config|
12
- config.root = 'en.errors'.freeze
14
+ config.root = '%{locale}.errors'.freeze
13
15
  end
14
16
 
15
17
  def self.load(paths = config.paths)
@@ -27,15 +29,16 @@ module Dry
27
29
  end
28
30
 
29
31
  def initialize(data)
32
+ super()
30
33
  @data = data
31
34
  end
32
35
 
33
- def get(key, _options = {})
34
- data[key]
36
+ def get(key, options = {})
37
+ data[key % { locale: options.fetch(:locale, :en) }]
35
38
  end
36
39
 
37
- def key?(key, *args)
38
- data.key?(key)
40
+ def key?(key, options = {})
41
+ data.key?(key % { locale: options.fetch(:locale, :en) })
39
42
  end
40
43
 
41
44
  def merge(overrides)
@@ -1,53 +1,61 @@
1
1
  module Dry
2
2
  module Validation
3
3
  class Result
4
+ include Dry::Equalizer(:output, :messages)
4
5
  include Enumerable
5
6
 
6
- attr_reader :rule_results
7
+ attr_reader :output
8
+ attr_reader :errors
9
+ attr_reader :error_compiler
10
+ attr_reader :hint_compiler
7
11
 
8
- def initialize(rule_results)
9
- @rule_results = rule_results
12
+ alias_method :to_hash, :output
13
+
14
+ EMPTY_MESSAGES = {}.freeze
15
+
16
+ def initialize(output, errors, error_compiler, hint_compiler)
17
+ @output = output
18
+ @errors = errors
19
+ @error_compiler = error_compiler
20
+ @hint_compiler = hint_compiler
10
21
  end
11
22
 
12
23
  def each(&block)
13
- rule_results.each(&block)
24
+ output.each(&block)
14
25
  end
15
26
 
16
27
  def [](name)
17
- to_h[name]
28
+ output.fetch(name)
18
29
  end
19
30
 
20
- def to_h
21
- @to_h ||= each_with_object({}) { |result, hash| hash[result.name] = result }
31
+ def success?
32
+ errors.empty?
22
33
  end
23
34
 
24
- def merge!(other)
25
- rule_results.concat(other.rule_results)
35
+ def failure?
36
+ !success?
26
37
  end
27
38
 
28
- def to_ary
29
- failures.map(&:to_ary)
30
- end
39
+ def messages(options = {})
40
+ @messages ||=
41
+ begin
42
+ return EMPTY_MESSAGES if success?
31
43
 
32
- def <<(rule_result)
33
- rule_results << rule_result
34
- end
44
+ hints = hint_compiler.with(options).call
45
+ comp = error_compiler.with(options.merge(hints: hints))
35
46
 
36
- def with_values(names, &block)
37
- values = names.map { |name| by_name(name) }.compact.map(&:input)
38
- yield(values) if values.size == names.size
47
+ comp.(error_ast)
48
+ end
39
49
  end
40
50
 
41
- def by_name(name)
42
- successes.detect { |rule_result| rule_result.name == name }
51
+ def to_ast
52
+ [:set, error_ast]
43
53
  end
44
54
 
45
- def successes
46
- rule_results.select(&:success?)
47
- end
55
+ private
48
56
 
49
- def failures
50
- rule_results.select(&:failure?)
57
+ def error_ast
58
+ errors.map { |error| error.to_ast }
51
59
  end
52
60
  end
53
61
  end
@@ -1,45 +1,73 @@
1
- require 'dry/logic/predicates'
2
- require 'dry/logic/rule_compiler'
1
+ require 'dry/types/constraints'
2
+
3
+ require 'dry/validation/schema_compiler'
4
+ require 'dry/validation/schema/key'
5
+ require 'dry/validation/schema/attr'
6
+ require 'dry/validation/schema/value'
7
+ require 'dry/validation/schema/check'
3
8
 
4
- require 'dry/validation/schema/definition'
5
9
  require 'dry/validation/error'
10
+ require 'dry/validation/result'
6
11
  require 'dry/validation/messages'
7
12
  require 'dry/validation/error_compiler'
8
13
  require 'dry/validation/hint_compiler'
9
- require 'dry/validation/result'
10
- require 'dry/validation/schema/result'
14
+
15
+ require 'dry/validation/input_processor_compiler'
11
16
 
12
17
  module Dry
13
18
  module Validation
14
19
  class Schema
15
20
  extend Dry::Configurable
16
- extend Definition
17
21
 
18
- setting :predicates, Logic::Predicates
22
+ NOOP_INPUT_PROCESSOR = -> input { input }
23
+
24
+ setting :path
25
+ setting :predicates, Types::Predicates
19
26
  setting :messages, :yaml
20
27
  setting :messages_file
21
28
  setting :namespace
29
+ setting :rules, []
30
+ setting :checks, []
22
31
 
23
- def self.predicates
24
- config.predicates
32
+ setting :input_processor, :noop
33
+
34
+ setting :input_processor_map, {
35
+ sanitizer: InputProcessorCompiler::Sanitizer.new,
36
+ form: InputProcessorCompiler::Form.new
37
+ }.freeze
38
+
39
+ def self.inherited(klass)
40
+ super
41
+ klass.setting :options, {}
25
42
  end
26
43
 
27
- def self.error_compiler
28
- ErrorCompiler.new(messages)
44
+ def self.new(rules = config.rules, **options)
45
+ super(rules, default_options.merge(options))
29
46
  end
30
47
 
31
- def self.hint_compiler
32
- HintCompiler.new(messages, rules: rules.map(&:to_ary))
48
+ def self.option(name, default = nil)
49
+ attr_reader(*name)
50
+ options.update(name => default)
51
+ end
52
+
53
+ def self.to_ast
54
+ [:schema, self]
55
+ end
56
+
57
+ def self.rules
58
+ config.rules
59
+ end
60
+
61
+ def self.predicates
62
+ config.predicates
63
+ end
64
+
65
+ def self.options
66
+ config.options
33
67
  end
34
68
 
35
69
  def self.messages
36
- default =
37
- case config.messages
38
- when :yaml then Messages.default
39
- when :i18n then Messages::I18n.new
40
- else
41
- fail "+#{config.messages}+ is not a valid messages identifier"
42
- end
70
+ default = default_messages
43
71
 
44
72
  if config.messages_file && config.namespace
45
73
  default.merge(config.messages_file).namespaced(config.namespace)
@@ -52,23 +80,61 @@ module Dry
52
80
  end
53
81
  end
54
82
 
55
- def self.rules
56
- @__rules__ ||= []
83
+ def self.default_messages
84
+ case config.messages
85
+ when :yaml then Messages.default
86
+ when :i18n then Messages::I18n.new
87
+ else
88
+ raise "+#{config.messages}+ is not a valid messages identifier"
89
+ end
90
+ end
91
+
92
+ def self.error_compiler
93
+ @error_compiler ||= ErrorCompiler.new(messages)
94
+ end
95
+
96
+ def self.hint_compiler
97
+ @hint_compiler ||= HintCompiler.new(messages, rules: rule_ast)
98
+ end
99
+
100
+ def self.input_processor
101
+ @input_processor ||=
102
+ begin
103
+ if input_processor_compiler
104
+ input_processor_compiler.(rule_ast)
105
+ else
106
+ NOOP_INPUT_PROCESSOR
107
+ end
108
+ end
109
+ end
110
+
111
+ def self.input_processor_ast(type)
112
+ config.input_processor_map.fetch(type).schema_ast(rule_ast)
57
113
  end
58
114
 
59
- def self.schemas
60
- @__schemas__ ||= []
115
+ def self.input_processor_compiler
116
+ @input_processor_comp ||= config.input_processor_map[config.input_processor]
61
117
  end
62
118
 
63
- def self.groups
64
- @__groups__ ||= []
119
+ def self.rule_ast
120
+ @rule_ast ||= config.rules.flat_map(&:rules).map(&:to_ast)
65
121
  end
66
122
 
67
- def self.checks
68
- @__checks__ ||= []
123
+ def self.default_options
124
+ { predicates: predicates,
125
+ error_compiler: error_compiler,
126
+ hint_compiler: hint_compiler,
127
+ input_processor: input_processor,
128
+ checks: config.checks }
69
129
  end
70
130
 
71
- attr_reader :rules, :schemas, :groups, :checks
131
+ attr_reader :rules
132
+
133
+ attr_reader :checks
134
+
135
+ attr_reader :predicates
136
+
137
+ attr_reader :input_processor
72
138
 
73
139
  attr_reader :rule_compiler
74
140
 
@@ -76,55 +142,96 @@ module Dry
76
142
 
77
143
  attr_reader :hint_compiler
78
144
 
79
- def initialize(rules = [])
80
- @rule_compiler = Logic::RuleCompiler.new(self)
81
- @rules = rule_compiler.(self.class.rules.map(&:to_ary) + rules.map(&:to_ary))
82
- @checks = self.class.checks.map(&:to_ary)
83
- @groups = rule_compiler.(self.class.groups.map(&:to_ary))
84
- @schemas = self.class.schemas.map(&:new)
85
- @error_compiler = self.class.error_compiler
86
- @hint_compiler = self.class.hint_compiler
145
+ attr_reader :options
146
+
147
+ def initialize(rules, options)
148
+ @rule_compiler = SchemaCompiler.new(self)
149
+ @error_compiler = options.fetch(:error_compiler)
150
+ @hint_compiler = options.fetch(:hint_compiler)
151
+ @predicates = options.fetch(:predicates)
152
+ @input_processor = options.fetch(:input_processor, NOOP_INPUT_PROCESSOR)
153
+
154
+ initialize_options(options)
155
+ initialize_rules(rules)
156
+ initialize_checks(options.fetch(:checks, []))
157
+
158
+ freeze
159
+ end
160
+
161
+ def with(new_options)
162
+ self.class.new(self.class.rules, options.merge(new_options))
87
163
  end
88
164
 
89
165
  def call(input)
90
- result = Validation::Result.new(rules.map { |rule| rule.(input) })
166
+ processed_input = input_processor[input]
167
+ Result.new(processed_input, apply(processed_input), error_compiler, hint_compiler)
168
+ end
91
169
 
92
- schemas.each do |schema|
93
- result.merge!(schema.(input).result)
170
+ def [](name)
171
+ if predicates.key?(name)
172
+ predicates[name]
173
+ elsif respond_to?(name)
174
+ Logic::Predicate.new(name, &method(name))
175
+ else
176
+ raise ArgumentError, "+#{name}+ is not a valid predicate name"
94
177
  end
178
+ end
95
179
 
96
- if checks.size > 0
97
- resolver = -> name { result[name] || self[name] }
98
- compiled_checks = Logic::RuleCompiler.new(resolver).(checks)
180
+ private
99
181
 
100
- compiled_checks.each do |rule|
101
- result << rule.(result)
102
- end
182
+ def apply(input)
183
+ results = rule_results(input)
184
+
185
+ results.merge!(check_results(input, results)) unless checks.empty?
186
+
187
+ results
188
+ .select { |_, result| result.failure? }
189
+ .map { |name, result| Error.new(error_path(name), result) }
190
+ end
191
+
192
+ def error_path(name)
193
+ full_path = Array[*self.class.config.path]
194
+ full_path << name
195
+ full_path.size > 1 ? full_path : full_path[0]
196
+ end
197
+
198
+ def rule_results(input)
199
+ rules.each_with_object({}) do |(name, rule), hash|
200
+ hash[name] = rule.(input)
103
201
  end
202
+ end
104
203
 
105
- groups.each do |group|
106
- result.with_values(group.rules) do |values|
107
- result << group.(*values)
108
- end
204
+ def check_results(input, result)
205
+ checks.each_with_object({}) do |(name, check), hash|
206
+ check_res = check.is_a?(Guard) ? check.(input, result) : check.(input)
207
+ hash[name] = check_res if check_res
109
208
  end
209
+ end
210
+
211
+ def initialize_options(options)
212
+ @options = options
110
213
 
111
- errors = Error::Set.new(result.failures.map { |failure| Error.new(failure) })
214
+ self.class.options.each do |name, default|
215
+ value = options.fetch(name) do
216
+ case default
217
+ when Proc then default.()
218
+ else default end
219
+ end
112
220
 
113
- Schema::Result.new(input, result, errors, error_compiler, hint_compiler)
221
+ instance_variable_set("@#{name}", value)
222
+ end
114
223
  end
115
224
 
116
- def [](name)
117
- if predicates.key?(name)
118
- predicates[name]
119
- elsif respond_to?(name)
120
- Logic::Predicate.new(name, &method(name))
121
- else
122
- fail ArgumentError, "+#{name}+ is not a valid predicate name"
225
+ def initialize_rules(rules)
226
+ @rules = rules.each_with_object({}) do |rule, result|
227
+ result[rule.name] = rule_compiler.visit(rule.to_ast)
123
228
  end
124
229
  end
125
230
 
126
- def predicates
127
- self.class.predicates
231
+ def initialize_checks(checks)
232
+ @checks = checks.each_with_object({}) do |check, result|
233
+ result[check.name] = rule_compiler.visit(check.to_ast)
234
+ end
128
235
  end
129
236
  end
130
237
  end