dry-schema 1.4.1 → 1.5.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +210 -73
  3. data/LICENSE +1 -1
  4. data/README.md +4 -6
  5. data/config/errors.yml +4 -0
  6. data/dry-schema.gemspec +46 -0
  7. data/lib/dry-schema.rb +1 -1
  8. data/lib/dry/schema.rb +20 -7
  9. data/lib/dry/schema/compiler.rb +5 -5
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +16 -7
  12. data/lib/dry/schema/dsl.rb +89 -31
  13. data/lib/dry/schema/extensions.rb +10 -2
  14. data/lib/dry/schema/extensions/hints.rb +15 -8
  15. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +2 -2
  16. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  17. data/lib/dry/schema/extensions/info.rb +27 -0
  18. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  19. data/lib/dry/schema/extensions/monads.rb +1 -1
  20. data/lib/dry/schema/extensions/struct.rb +32 -0
  21. data/lib/dry/schema/json.rb +1 -1
  22. data/lib/dry/schema/key.rb +20 -5
  23. data/lib/dry/schema/key_coercer.rb +4 -4
  24. data/lib/dry/schema/key_map.rb +9 -4
  25. data/lib/dry/schema/key_validator.rb +67 -0
  26. data/lib/dry/schema/macros.rb +8 -8
  27. data/lib/dry/schema/macros/array.rb +17 -4
  28. data/lib/dry/schema/macros/core.rb +11 -6
  29. data/lib/dry/schema/macros/dsl.rb +44 -23
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -5
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -9
  34. data/lib/dry/schema/macros/maybe.rb +4 -5
  35. data/lib/dry/schema/macros/optional.rb +1 -1
  36. data/lib/dry/schema/macros/required.rb +1 -1
  37. data/lib/dry/schema/macros/schema.rb +23 -2
  38. data/lib/dry/schema/macros/value.rb +34 -7
  39. data/lib/dry/schema/message.rb +35 -9
  40. data/lib/dry/schema/message/or.rb +18 -39
  41. data/lib/dry/schema/message/or/abstract.rb +28 -0
  42. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  43. data/lib/dry/schema/message/or/single_path.rb +64 -0
  44. data/lib/dry/schema/message_compiler.rb +58 -22
  45. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  46. data/lib/dry/schema/message_set.rb +26 -37
  47. data/lib/dry/schema/messages.rb +6 -6
  48. data/lib/dry/schema/messages/abstract.rb +54 -62
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +12 -2
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +61 -14
  53. data/lib/dry/schema/params.rb +1 -1
  54. data/lib/dry/schema/path.rb +44 -5
  55. data/lib/dry/schema/predicate.rb +4 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +2 -2
  58. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  59. data/lib/dry/schema/processor.rb +50 -29
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +53 -6
  62. data/lib/dry/schema/rule_applier.rb +7 -7
  63. data/lib/dry/schema/step.rb +79 -0
  64. data/lib/dry/schema/trace.rb +5 -4
  65. data/lib/dry/schema/type_container.rb +3 -3
  66. data/lib/dry/schema/type_registry.rb +2 -2
  67. data/lib/dry/schema/types.rb +1 -1
  68. data/lib/dry/schema/value_coercer.rb +2 -2
  69. data/lib/dry/schema/version.rb +1 -1
  70. metadata +21 -7
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
- require 'pathname'
3
+ require "yaml"
4
+ require "pathname"
5
5
 
6
- require 'dry/equalizer'
7
- require 'dry/schema/constants'
8
- require 'dry/schema/messages/abstract'
6
+ require "dry/equalizer"
7
+ require "dry/schema/constants"
8
+ require "dry/schema/messages/abstract"
9
9
 
10
10
  module Dry
11
11
  module Schema
@@ -13,7 +13,13 @@ module Dry
13
13
  #
14
14
  # @api public
15
15
  class Messages::YAML < Messages::Abstract
16
- LOCALE_TOKEN = '%<locale>s'
16
+ LOCALE_TOKEN = "%<locale>s"
17
+ TOKEN_REGEXP = /%{(\w*)}/.freeze
18
+ EMPTY_CONTEXT = Object.new.tap { |ctx|
19
+ def ctx.context
20
+ binding
21
+ end
22
+ }.freeze.context
17
23
 
18
24
  include Dry::Equalizer(:data)
19
25
 
@@ -22,7 +28,7 @@ module Dry
22
28
  # @return [Hash]
23
29
  attr_reader :data
24
30
 
25
- # Translation function
31
+ # Translation function
26
32
  #
27
33
  # @return [Proc]
28
34
  attr_reader :t
@@ -45,21 +51,26 @@ module Dry
45
51
  hash.each do |key, value|
46
52
  flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
47
53
 
48
- if value.is_a?(String) && hash['text'] != value
54
+ if value.is_a?(String) && hash["text"] != value
49
55
  keys[[*path, key].join(DOT)] = {
50
56
  text: value,
51
57
  meta: EMPTY_HASH
52
58
  }
53
- elsif value.is_a?(Hash) && value['text'].is_a?(String)
59
+ elsif value.is_a?(Hash) && value["text"].is_a?(String)
54
60
  keys[[*path, key].join(DOT)] = {
55
- text: value['text'],
56
- meta: value.dup.delete_if { |k| k == 'text' }.map { |k, v| [k.to_sym, v] }.to_h
61
+ text: value["text"],
62
+ meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h
57
63
  }
58
64
  end
59
65
  end
60
66
  keys
61
67
  end
62
68
 
69
+ # @api private
70
+ def self.cache
71
+ @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
72
+ end
73
+
63
74
  # @api private
64
75
  def initialize(data: EMPTY_HASH, config: nil)
65
76
  super()
@@ -77,7 +88,7 @@ module Dry
77
88
  #
78
89
  # @api public
79
90
  def looked_up_paths(predicate, options)
80
- super.map { |path| path % { locale: options[:locale] || default_locale } }
91
+ super.map { |path| path % {locale: options[:locale] || default_locale} }
81
92
  end
82
93
 
83
94
  # Get a message for the given key and its options
@@ -124,12 +135,48 @@ module Dry
124
135
 
125
136
  # @api private
126
137
  def prepare
127
- @data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge)
138
+ @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
128
139
  self
129
140
  end
130
141
 
142
+ # @api private
143
+ def interpolatable_data(key, options, **data)
144
+ tokens = evaluation_context(key, options).fetch(:tokens)
145
+ data.select { |k,| tokens.include?(k) }
146
+ end
147
+
148
+ # @api private
149
+ def interpolate(key, options, **data)
150
+ evaluator = evaluation_context(key, options).fetch(:evaluator)
151
+ data.empty? ? evaluator.() : evaluator.(**data)
152
+ end
153
+
131
154
  private
132
155
 
156
+ # @api private
157
+ def evaluation_context(key, options)
158
+ cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
159
+ tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
160
+ text = input.gsub("%", "#")
161
+
162
+ # rubocop:disable Security/Eval
163
+ evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
164
+ -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
165
+ RUBY
166
+ # rubocop:enable Security/Eval
167
+
168
+ {
169
+ tokens: tokens,
170
+ evaluator: evaluator
171
+ }
172
+ end
173
+ end
174
+
175
+ # @api private
176
+ def cache
177
+ @cache ||= self.class.cache[self]
178
+ end
179
+
133
180
  # @api private
134
181
  def load_translations(path)
135
182
  data = self.class.flat_hash(YAML.load_file(path))
@@ -143,7 +190,7 @@ module Dry
143
190
  def evaluated_key(key, options)
144
191
  return key unless key.include?(LOCALE_TOKEN)
145
192
 
146
- key % { locale: options[:locale] || default_locale }
193
+ key % {locale: options[:locale] || default_locale}
147
194
  end
148
195
  end
149
196
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/processor'
3
+ require "dry/schema/processor"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/constants'
3
+ require "dry/schema/constants"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -8,6 +8,7 @@ module Dry
8
8
  #
9
9
  # @api private
10
10
  class Path
11
+ include Dry.Equalizer(:keys)
11
12
  include Comparable
12
13
  include Enumerable
13
14
 
@@ -23,7 +24,7 @@ module Dry
23
24
  # @return [Path]
24
25
  #
25
26
  # @api private
26
- def self.[](spec)
27
+ def self.call(spec)
27
28
  case spec
28
29
  when Symbol, Array
29
30
  new(Array[*spec])
@@ -34,10 +35,15 @@ module Dry
34
35
  when Path
35
36
  spec
36
37
  else
37
- raise ArgumentError, '+spec+ must be either a Symbol, Array, Hash or a Path'
38
+ raise ArgumentError, "+spec+ must be either a Symbol, Array, Hash or a Path"
38
39
  end
39
40
  end
40
41
 
42
+ # @api private
43
+ def self.[](spec)
44
+ call(spec)
45
+ end
46
+
41
47
  # Extract a list of keys from a hash
42
48
  #
43
49
  # @api private
@@ -52,6 +58,28 @@ module Dry
52
58
  @keys = keys
53
59
  end
54
60
 
61
+ # @api private
62
+ def to_h(value = EMPTY_ARRAY.dup)
63
+ curr_idx = 0
64
+ last_idx = keys.size - 1
65
+ hash = EMPTY_HASH.dup
66
+ node = hash
67
+
68
+ while curr_idx <= last_idx
69
+ node =
70
+ node[keys[curr_idx]] =
71
+ if curr_idx == last_idx
72
+ value.is_a?(Array) ? value : [value]
73
+ else
74
+ EMPTY_HASH.dup
75
+ end
76
+
77
+ curr_idx += 1
78
+ end
79
+
80
+ hash
81
+ end
82
+
55
83
  # @api private
56
84
  def each(&block)
57
85
  keys.each(&block)
@@ -83,8 +111,19 @@ module Dry
83
111
  end
84
112
 
85
113
  # @api private
86
- def key_matches(other)
87
- map { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
114
+ def &(other)
115
+ unless same_root?(other)
116
+ raise ArgumentError, "#{other.inspect} doesn't have the same root #{inspect}"
117
+ end
118
+
119
+ self.class.new(
120
+ key_matches(other, :select).compact.reject { |value| value.equal?(false) }
121
+ )
122
+ end
123
+
124
+ # @api private
125
+ def key_matches(other, meth = :map)
126
+ public_send(meth) { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
88
127
  end
89
128
 
90
129
  # @api private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/logic/operators'
3
+ require "dry/equalizer"
4
+ require "dry/logic/operators"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -13,6 +13,8 @@ module Dry
13
13
  #
14
14
  # @api private
15
15
  class Negation
16
+ include Dry::Logic::Operators
17
+
16
18
  # @api private
17
19
  attr_reader :predicate
18
20
 
@@ -1,196 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
3
+ require "dry/types/predicate_inferrer"
4
4
 
5
5
  module Dry
6
6
  module Schema
7
- # PredicateInferrer is used internally by `Macros::Value`
8
- # for inferring type-check predicates from type specs.
9
- #
10
7
  # @api private
11
- class PredicateInferrer
12
- extend Dry::Core::Cache
8
+ class PredicateInferrer < ::Dry::Types::PredicateInferrer
9
+ Compiler = ::Class.new(superclass::Compiler)
13
10
 
14
- TYPE_TO_PREDICATE = {
15
- DateTime => :date_time?,
16
- FalseClass => :false?,
17
- Integer => :int?,
18
- NilClass => :nil?,
19
- String => :str?,
20
- TrueClass => :true?,
21
- BigDecimal => :decimal?
22
- }.freeze
23
-
24
- REDUCED_TYPES = {
25
- [[[:true?], [:false?]]] => %i[bool?]
26
- }.freeze
27
-
28
- HASH = %i[hash?].freeze
29
-
30
- ARRAY = %i[array?].freeze
31
-
32
- NIL = %i[nil?].freeze
33
-
34
- # Compiler reduces type AST into a list of predicates
35
- #
36
- # @api private
37
- class Compiler
38
- # @return [PredicateRegistry]
39
- # @api private
40
- attr_reader :registry
41
-
42
- # @api private
43
- def initialize(registry)
44
- @registry = registry
45
- end
46
-
47
- # @api private
48
- def infer_predicate(type)
49
- [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
50
- end
51
-
52
- # @api private
53
- def visit(node)
54
- meth, rest = node
55
- public_send(:"visit_#{meth}", rest)
56
- end
57
-
58
- # @api private
59
- def visit_nominal(node)
60
- type = node[0]
61
- predicate = infer_predicate(type)
62
-
63
- if registry.key?(predicate[0])
64
- predicate
65
- else
66
- [type?: type]
67
- end
68
- end
69
-
70
- # @api private
71
- def visit_hash(_)
72
- HASH
73
- end
74
-
75
- # @api private
76
- def visit_array(_)
77
- ARRAY
78
- end
79
-
80
- # @api private
81
- def visit_lax(node)
82
- visit(node)
83
- end
84
-
85
- # @api private
86
- def visit_constructor(node)
87
- other, * = node
88
- visit(other)
89
- end
90
-
91
- # @api private
92
- def visit_enum(node)
93
- other, * = node
94
- visit(other)
95
- end
96
-
97
- # @api private
98
- def visit_sum(node)
99
- left_node, right_node, = node
100
- left = visit(left_node)
101
- right = visit(right_node)
102
-
103
- if left.eql?(NIL)
104
- right
105
- else
106
- [[left, right]]
107
- end
108
- end
109
-
110
- # @api private
111
- def visit_constrained(node)
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
120
- end
121
-
122
- # @api private
123
- def visit_any(_)
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]
166
- end
167
- end
168
-
169
- # @return [Compiler]
170
- # @api private
171
- attr_reader :compiler
172
-
173
- # @api private
174
- def initialize(registry)
11
+ def initialize(registry = PredicateRegistry.new)
175
12
  @compiler = Compiler.new(registry)
176
13
  end
177
-
178
- # Infer predicate identifier from the provided type
179
- #
180
- # @return [Symbol]
181
- #
182
- # @api private
183
- def [](type)
184
- self.class.fetch_or_store(type.hash) do
185
- predicates = compiler.visit(type.to_ast)
186
-
187
- if predicates.is_a?(Hash)
188
- predicates
189
- else
190
- REDUCED_TYPES[predicates] || predicates
191
- end
192
- end
193
- end
194
14
  end
195
15
  end
196
16
  end