dry-schema 1.4.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
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