dry-schema 1.2.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -60,7 +60,7 @@ module Dry
60
60
  #
61
61
  # If a string is passed, it will be compared with the text
62
62
  #
63
- # @param [Message,String]
63
+ # @param other [Message,String]
64
64
  #
65
65
  # @return [Boolean]
66
66
  #
@@ -130,7 +130,8 @@ module Dry
130
130
  path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
131
131
  ).to_h
132
132
 
133
- template, meta = messages[predicate, options] || raise(MissingMessageError, path)
133
+ template, meta = messages[predicate, options] ||
134
+ raise(MissingMessageError.new(path, messages.looked_up_paths(predicate, options)))
134
135
 
135
136
  text = message_text(template, tokens, options)
136
137
 
@@ -92,30 +92,34 @@ module Dry
92
92
  #
93
93
  # @api public
94
94
  def call(predicate, options)
95
- cache.fetch_or_store([predicate, options.reject { |k,| k.equal?(:input) }]) do
95
+ cache.fetch_or_store(cache_key(predicate, options)) do
96
96
  text, meta = lookup(predicate, options)
97
97
  [Template[text], meta] if text
98
98
  end
99
99
  end
100
100
  alias_method :[], :call
101
101
 
102
+ # Retrieve an array of looked up paths
103
+ #
104
+ # @param [Symbol] predicate
105
+ # @param [Hash] options
106
+ #
107
+ # @return [String]
108
+ #
109
+ # @api public
110
+ def looked_up_paths(predicate, options)
111
+ tokens = lookup_tokens(predicate, options)
112
+ filled_lookup_paths(tokens)
113
+ end
114
+
102
115
  # Try to find a message for the given predicate and its options
103
116
  #
104
117
  # @api private
105
118
  #
106
119
  # rubocop:disable Metrics/AbcSize
107
120
  def lookup(predicate, options)
108
- tokens = options.merge(
109
- predicate: predicate,
110
- root: options[:not] ? "#{root}.not" : root,
111
- arg_type: config.arg_types[options[:arg_type]],
112
- val_type: config.val_types[options[:val_type]],
113
- message_type: options[:message_type] || :failure
114
- )
115
-
116
121
  opts = options.reject { |k, _| config.lookup_options.include?(k) }
117
-
118
- path = lookup_paths(tokens).detect { |key| key?(key, opts) }
122
+ path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
119
123
 
120
124
  return unless path
121
125
 
@@ -130,7 +134,13 @@ module Dry
130
134
  # rubocop:enable Metrics/AbcSize
131
135
 
132
136
  # @api private
133
- def lookup_paths(tokens)
137
+ def lookup_paths(predicate, options)
138
+ tokens = lookup_tokens(predicate, options)
139
+ filled_lookup_paths(tokens)
140
+ end
141
+
142
+ # @api private
143
+ def filled_lookup_paths(tokens)
134
144
  config.lookup_paths.map { |path| path % tokens }
135
145
  end
136
146
 
@@ -167,8 +177,28 @@ module Dry
167
177
  config.default_locale
168
178
  end
169
179
 
180
+ # @api private
181
+ def cache_key(predicate, options)
182
+ if options.key?(:input)
183
+ [predicate, options.reject { |k,| k.equal?(:input) }]
184
+ else
185
+ [predicate, options]
186
+ end
187
+ end
188
+
170
189
  private
171
190
 
191
+ # @api private
192
+ def lookup_tokens(predicate, options)
193
+ options.merge(
194
+ predicate: predicate,
195
+ root: options[:not] ? "#{root}.not" : root,
196
+ arg_type: config.arg_types[options[:arg_type]],
197
+ val_type: config.val_types[options[:val_type]],
198
+ message_type: options[:message_type] || :failure
199
+ )
200
+ end
201
+
172
202
  # @api private
173
203
  def custom_top_namespace?(path)
174
204
  path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
@@ -79,6 +79,15 @@ module Dry
79
79
  self
80
80
  end
81
81
 
82
+ # @api private
83
+ def cache_key(predicate, options)
84
+ if options[:locale]
85
+ super
86
+ else
87
+ [*super, I18n.locale]
88
+ end
89
+ end
90
+
82
91
  private
83
92
 
84
93
  # @api private
@@ -55,7 +55,7 @@ module Dry
55
55
  end
56
56
 
57
57
  # @api private
58
- def lookup_paths(tokens)
58
+ def filled_lookup_paths(tokens)
59
59
  super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
60
60
  end
61
61
 
@@ -64,6 +64,11 @@ module Dry
64
64
  base_paths = messages.rule_lookup_paths(tokens)
65
65
  base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
66
66
  end
67
+
68
+ # @api private
69
+ def cache_key(predicate, options)
70
+ messages.cache_key(predicate, options)
71
+ end
67
72
  end
68
73
  end
69
74
  end
@@ -68,6 +68,18 @@ module Dry
68
68
  @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
69
69
  end
70
70
 
71
+ # Get an array of looked up paths
72
+ #
73
+ # @param [Symbol] predicate
74
+ # @param [Hash] options
75
+ #
76
+ # @return [String]
77
+ #
78
+ # @api public
79
+ def looked_up_paths(predicate, options)
80
+ super.map { |path| path % { locale: options[:locale] || default_locale } }
81
+ end
82
+
71
83
  # Get a message for the given key and its options
72
84
  #
73
85
  # @param [Symbol] key
@@ -29,9 +29,10 @@ module Dry
29
29
  end
30
30
 
31
31
  # @api private
32
- def ast(input=Undefined)
32
+ def ast(input = Undefined)
33
33
  [:namespace, [namespace, rule.ast(input)]]
34
34
  end
35
+ alias_method :to_ast, :ast
35
36
  end
36
37
  end
37
38
  end
@@ -65,8 +65,10 @@ module Dry
65
65
  # @api private
66
66
  def include?(other)
67
67
  return false unless same_root?(other)
68
- return false if index? && other.index? && !last.equal?(other.last)
69
- self >= other
68
+ return last.equal?(other.last) if index? && other.index?
69
+ return self.class.new([*to_a[0..-2]]).include?(other) if index?
70
+
71
+ self >= other && !other.key_matches(self).include?(nil)
70
72
  end
71
73
 
72
74
  # @api private
@@ -75,14 +77,16 @@ module Dry
75
77
 
76
78
  return 0 if keys.eql?(other.keys)
77
79
 
78
- res =
79
- map { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
80
- .compact
81
- .reject { |value| value.equal?(false) }
80
+ res = key_matches(other).compact.reject { |value| value.equal?(false) }
82
81
 
83
82
  res.size < count ? 1 : -1
84
83
  end
85
84
 
85
+ # @api private
86
+ def key_matches(other)
87
+ map { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
88
+ end
89
+
86
90
  # @api private
87
91
  def last
88
92
  keys.last
@@ -22,9 +22,15 @@ module Dry
22
22
  }.freeze
23
23
 
24
24
  REDUCED_TYPES = {
25
- %i[true? false?] => :bool?
25
+ [[[:true?], [:false?]]] => %i[bool?]
26
26
  }.freeze
27
27
 
28
+ HASH = %i[hash?].freeze
29
+
30
+ ARRAY = %i[array?].freeze
31
+
32
+ NIL = %i[nil?].freeze
33
+
28
34
  # Compiler reduces type AST into a list of predicates
29
35
  #
30
36
  # @api private
@@ -40,7 +46,7 @@ module Dry
40
46
 
41
47
  # @api private
42
48
  def infer_predicate(type)
43
- TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }
49
+ [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
44
50
  end
45
51
 
46
52
  # @api private
@@ -54,21 +60,21 @@ module Dry
54
60
  type = node[0]
55
61
  predicate = infer_predicate(type)
56
62
 
57
- if registry.key?(predicate)
63
+ if registry.key?(predicate[0])
58
64
  predicate
59
65
  else
60
- { type?: type }
66
+ [type?: type]
61
67
  end
62
68
  end
63
69
 
64
70
  # @api private
65
71
  def visit_hash(_)
66
- :hash?
72
+ HASH
67
73
  end
68
74
 
69
75
  # @api private
70
76
  def visit_array(_)
71
- :array?
77
+ ARRAY
72
78
  end
73
79
 
74
80
  # @api private
@@ -90,26 +96,73 @@ module Dry
90
96
 
91
97
  # @api private
92
98
  def visit_sum(node)
93
- left, right = node
99
+ left_node, right_node, = node
100
+ left = visit(left_node)
101
+ right = visit(right_node)
94
102
 
95
- predicates = [visit(left), visit(right)]
96
-
97
- if predicates.first == :nil?
98
- predicates[1..predicates.size - 1]
103
+ if left.eql?(NIL)
104
+ right
99
105
  else
100
- predicates
106
+ [[left, right]]
101
107
  end
102
108
  end
103
109
 
104
110
  # @api private
105
111
  def visit_constrained(node)
106
- other, * = node
107
- visit(other)
112
+ other, rules = node
113
+ predicates = visit(rules)
114
+
115
+ if predicates.empty?
116
+ visit(other)
117
+ else
118
+ [*visit(other), *merge_predicates(predicates)]
119
+ end
108
120
  end
109
121
 
110
122
  # @api private
111
123
  def visit_any(_)
112
- nil
124
+ EMPTY_ARRAY
125
+ end
126
+
127
+ # @api private
128
+ def visit_and(node)
129
+ left, right = node
130
+ visit(left) + visit(right)
131
+ end
132
+
133
+ # @api private
134
+ def visit_predicate(node)
135
+ pred, args = node
136
+
137
+ if pred.equal?(:type?)
138
+ EMPTY_ARRAY
139
+ elsif registry.key?(pred)
140
+ *curried, _ = args
141
+ values = curried.map { |_, v| v }
142
+
143
+ if values.empty?
144
+ [pred]
145
+ else
146
+ [pred => values[0]]
147
+ end
148
+ else
149
+ EMPTY_ARRAY
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # @api private
156
+ def merge_predicates(nodes)
157
+ preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
158
+ if predicate.is_a?(::Hash)
159
+ h.update(predicate)
160
+ else
161
+ ps << predicate
162
+ end
163
+ end
164
+
165
+ merged.empty? ? preds : [*preds, merged]
113
166
  end
114
167
  end
115
168
 
@@ -134,7 +187,7 @@ module Dry
134
187
  if predicates.is_a?(Hash)
135
188
  predicates
136
189
  else
137
- Array(REDUCED_TYPES[predicates] || predicates).flatten
190
+ REDUCED_TYPES[predicates] || predicates
138
191
  end
139
192
  end
140
193
  end
@@ -1,35 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/logic/predicates'
4
+ require 'dry/types/predicate_registry'
4
5
 
5
6
  module Dry
6
7
  module Schema
7
8
  # A registry with predicate objects from `Dry::Logic::Predicates`
8
9
  #
9
10
  # @api private
10
- class PredicateRegistry
11
- # @api private
12
- attr_reader :predicates
13
-
14
- # @api private
15
- attr_reader :has_predicate
16
-
17
- # @api private
18
- def initialize(predicates = Dry::Logic::Predicates)
19
- @predicates = predicates
20
- @has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
21
- end
22
-
23
- # @api private
24
- def [](name)
25
- predicates[name]
26
- end
27
-
28
- # @api private
29
- def key?(name)
30
- has_predicate.(name)
31
- end
32
-
11
+ class PredicateRegistry < Dry::Types::PredicateRegistry
33
12
  # @api private
34
13
  def arg_list(name, *values)
35
14
  predicate = self[name]
@@ -5,6 +5,7 @@ require 'dry/initializer'
5
5
 
6
6
  require 'dry/schema/type_registry'
7
7
  require 'dry/schema/type_container'
8
+ require 'dry/schema/processor_steps'
8
9
  require 'dry/schema/rule_applier'
9
10
  require 'dry/schema/key_coercer'
10
11
  require 'dry/schema/value_coercer'
@@ -12,14 +13,9 @@ require 'dry/schema/value_coercer'
12
13
  module Dry
13
14
  module Schema
14
15
  # Processes input data using objects configured within the DSL
16
+ # Processing is split into steps represented by `ProcessorSteps`.
15
17
  #
16
- # Processing is split into 4 main steps:
17
- #
18
- # 1. Prepare input hash using a key map
19
- # 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
20
- # 3. Apply value coercions based on type specifications
21
- # 4. Apply rules
22
- #
18
+ # @see ProcessorSteps
23
19
  # @see Params
24
20
  # @see JSON
25
21
  #
@@ -29,10 +25,10 @@ module Dry
29
25
  extend Dry::Configurable
30
26
 
31
27
  setting :key_map_type
32
- setting :type_registry_namespace, :nominal
28
+ setting :type_registry_namespace, :strict
33
29
  setting :filter_empty_string, false
34
30
 
35
- option :steps, default: -> { EMPTY_ARRAY.dup }
31
+ option :steps, default: -> { ProcessorSteps.new }
36
32
 
37
33
  option :schema_dsl
38
34
 
@@ -77,16 +73,6 @@ module Dry
77
73
  end
78
74
  end
79
75
 
80
- # Append a step
81
- #
82
- # @return [Processor]
83
- #
84
- # @api private
85
- def <<(step)
86
- steps << step
87
- self
88
- end
89
-
90
76
  # Apply processing steps to the provided input
91
77
  #
92
78
  # @param [Hash] input
@@ -96,10 +82,7 @@ module Dry
96
82
  # @api public
97
83
  def call(input)
98
84
  Result.new(input, message_compiler: message_compiler) do |result|
99
- steps.each do |step|
100
- output = step.(result)
101
- result.replace(output) if output.is_a?(::Hash)
102
- end
85
+ steps.call(result)
103
86
  end
104
87
  end
105
88
  alias_method :[], :call
@@ -119,7 +102,7 @@ module Dry
119
102
  #
120
103
  # @api public
121
104
  def key_map
122
- @key_map ||= steps.detect { |s| s.is_a?(KeyCoercer) }.key_map
105
+ steps[:key_coercer].key_map
123
106
  end
124
107
 
125
108
  # Return string represntation
@@ -139,7 +122,7 @@ module Dry
139
122
  #
140
123
  # @api private
141
124
  def type_schema
142
- @type_schema ||= steps.detect { |s| s.is_a?(ValueCoercer) }.type_schema
125
+ steps[:value_coercer].type_schema
143
126
  end
144
127
 
145
128
  # Return the rules config
@@ -180,7 +163,7 @@ module Dry
180
163
  #
181
164
  # @api private
182
165
  def rule_applier
183
- @rule_applier ||= steps.last
166
+ steps[:rule_applier]
184
167
  end
185
168
  alias_method :to_rule, :rule_applier
186
169