dry-schema 1.2.0 → 1.4.1

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.
@@ -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