dry-schema 1.4.2 → 1.5.3

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 +217 -78
  3. data/LICENSE +1 -1
  4. data/README.md +4 -4
  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 +19 -6
  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 +87 -27
  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 +1 -1
  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 +16 -1
  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 +9 -4
  29. data/lib/dry/schema/macros/dsl.rb +34 -19
  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 +9 -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 +55 -19
  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 -56
  49. data/lib/dry/schema/messages/i18n.rb +29 -27
  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 +49 -28
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +52 -5
  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,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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/predicates'
4
- require 'dry/types/predicate_registry'
3
+ require "dry/logic/predicates"
4
+ require "dry/types/predicate_registry"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/primitive_inferrer"
4
+
5
+ module Dry
6
+ module Schema
7
+ # @api private
8
+ class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
9
+ Compiler = ::Class.new(superclass::Compiler)
10
+
11
+ def initialize
12
+ @compiler = Compiler.new
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable'
4
- require 'dry/initializer'
3
+ require "dry/configurable"
4
+ require "dry/initializer"
5
+ require "dry/logic/operators"
5
6
 
6
- require 'dry/schema/type_registry'
7
- require 'dry/schema/type_container'
8
- require 'dry/schema/processor_steps'
9
- require 'dry/schema/rule_applier'
10
- require 'dry/schema/key_coercer'
11
- require 'dry/schema/value_coercer'
7
+ require "dry/schema/type_registry"
8
+ require "dry/schema/type_container"
9
+ require "dry/schema/processor_steps"
10
+ require "dry/schema/rule_applier"
11
+ require "dry/schema/key_coercer"
12
+ require "dry/schema/value_coercer"
12
13
 
13
14
  module Dry
14
15
  module Schema
@@ -24,6 +25,8 @@ module Dry
24
25
  extend Dry::Initializer
25
26
  extend Dry::Configurable
26
27
 
28
+ include Dry::Logic::Operators
29
+
27
30
  setting :key_map_type
28
31
  setting :type_registry_namespace, :strict
29
32
  setting :filter_empty_string, false
@@ -68,7 +71,7 @@ module Dry
68
71
  elsif definition
69
72
  definition.call
70
73
  else
71
- raise ArgumentError, 'Cannot create a schema without a definition'
74
+ raise ArgumentError, "Cannot create a schema without a definition"
72
75
  end
73
76
  end
74
77
  end
@@ -81,28 +84,36 @@ module Dry
81
84
  #
82
85
  # @api public
83
86
  def call(input)
84
- Result.new(input, message_compiler: message_compiler) do |result|
87
+ Result.new(input, input: input, message_compiler: message_compiler) do |result|
85
88
  steps.call(result)
86
89
  end
87
90
  end
88
91
  alias_method :[], :call
89
92
 
90
- # Return a proc that acts like a schema object
93
+ # @api public
94
+ def xor(other)
95
+ raise NotImplementedError, "composing schemas using `xor` operator is not supported yet"
96
+ end
97
+ alias_method :^, :xor
98
+
99
+ # Merge with another schema
91
100
  #
92
- # @return [Proc]
101
+ # @param [Processor] other
102
+ #
103
+ # @return [Processor, Params, JSON]
93
104
  #
94
105
  # @api public
95
- def to_proc
96
- ->(input) { call(input) }
106
+ def merge(other)
107
+ schema_dsl.merge(other.schema_dsl).()
97
108
  end
98
109
 
99
- # Return the key map
110
+ # Return a proc that acts like a schema object
100
111
  #
101
- # @return [KeyMap]
112
+ # @return [Proc]
102
113
  #
103
114
  # @api public
104
- def key_map
105
- steps[:key_coercer].key_map
115
+ def to_proc
116
+ ->(input) { call(input) }
106
117
  end
107
118
 
108
119
  # Return string represntation
@@ -116,15 +127,32 @@ module Dry
116
127
  STR
117
128
  end
118
129
 
130
+ # Return the key map
131
+ #
132
+ # @return [KeyMap]
133
+ #
134
+ # @api public
135
+ def key_map
136
+ steps.key_map
137
+ end
138
+
119
139
  # Return the type schema
120
140
  #
121
141
  # @return [Dry::Types::Safe]
122
142
  #
123
143
  # @api private
124
144
  def type_schema
125
- steps[:value_coercer].type_schema
145
+ steps.type_schema
126
146
  end
127
147
 
148
+ # Return the rule applier
149
+ #
150
+ # @api private
151
+ def rule_applier
152
+ steps.rule_applier
153
+ end
154
+ alias_method :to_rule, :rule_applier
155
+
128
156
  # Return the rules config
129
157
  #
130
158
  # @return [Dry::Types::Config]
@@ -137,9 +165,10 @@ module Dry
137
165
  # Return AST representation of the rules
138
166
  #
139
167
  # @api private
140
- def to_ast
168
+ def to_ast(*)
141
169
  rule_applier.to_ast
142
170
  end
171
+ alias_method :ast, :to_ast
143
172
 
144
173
  # Return the message compiler
145
174
  #
@@ -159,14 +188,6 @@ module Dry
159
188
  rule_applier.rules
160
189
  end
161
190
 
162
- # Return the rule applier
163
- #
164
- # @api private
165
- def rule_applier
166
- steps[:rule_applier]
167
- end
168
- alias_method :to_rule, :rule_applier
169
-
170
191
  # Check if there are filter rules
171
192
  #
172
193
  # @api private