dry-schema 0.1.1 → 0.2.0

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.
@@ -35,10 +35,21 @@ module Dry
35
35
  end
36
36
  end
37
37
 
38
- # Specify a nested schema
38
+ # Specify a nested hash without enforced hash? type-check
39
39
  #
40
40
  # @api public
41
41
  def schema(*args, &block)
42
+ append_macro(Macros::Schema) do |macro|
43
+ macro.call(*args, &block)
44
+ end
45
+ end
46
+
47
+ # Specify a nested hash with enforced hash? type-check
48
+ #
49
+ # @see #schema
50
+ #
51
+ # @api public
52
+ def hash(*args, &block)
42
53
  append_macro(Macros::Hash) do |macro|
43
54
  macro.call(*args, &block)
44
55
  end
@@ -53,6 +64,16 @@ module Dry
53
64
  end
54
65
  end
55
66
 
67
+ # Like `each`, but prepends `array?` check
68
+ #
69
+ # @api public
70
+ def array(*args, &block)
71
+ value(:array)
72
+ append_macro(Macros::Each) do |macro|
73
+ macro.value(*args, &block)
74
+ end
75
+ end
76
+
56
77
  private
57
78
 
58
79
  # @api private
@@ -7,16 +7,20 @@ module Dry
7
7
  #
8
8
  # @api public
9
9
  class Filled < Value
10
- def call(*args, &block)
11
- if args.include?(:empty?)
10
+ def call(*predicates, **opts, &block)
11
+ if predicates.include?(:empty?)
12
12
  raise ::Dry::Schema::InvalidSchemaError, "Using filled with empty? predicate is invalid"
13
13
  end
14
14
 
15
- if args.include?(:filled?)
15
+ if predicates.include?(:filled?)
16
16
  raise ::Dry::Schema::InvalidSchemaError, "Using filled with filled? is redundant"
17
17
  end
18
18
 
19
- value(:filled?, *args, &block)
19
+ if opts[:type_spec].equal?(true)
20
+ value(predicates[0], :filled?, *predicates[1..predicates.size-1], **opts, &block)
21
+ else
22
+ value(:filled?, *predicates, **opts, &block)
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -1,4 +1,4 @@
1
- require 'dry/schema/macros/value'
1
+ require 'dry/schema/macros/schema'
2
2
 
3
3
  module Dry
4
4
  module Schema
@@ -6,38 +6,11 @@ module Dry
6
6
  # Macro used to specify a nested schema
7
7
  #
8
8
  # @api public
9
- class Hash < Value
9
+ class Hash < Schema
10
10
  # @api private
11
11
  def call(*args, &block)
12
12
  trace << hash?
13
-
14
- super(*args) unless args.empty?
15
-
16
- if block
17
- definition = schema_dsl.new(&block)
18
-
19
- parent_type = schema_dsl.types[name]
20
- definition_schema = definition.type_schema
21
-
22
- schema_type =
23
- if parent_type.respond_to?(:of)
24
- parent_type.of(definition_schema)
25
- else
26
- definition_schema
27
- end
28
-
29
- final_type =
30
- if schema_dsl.maybe?(parent_type)
31
- schema_type.optional
32
- else
33
- schema_type
34
- end
35
-
36
- schema_dsl.set_type(name, final_type)
37
-
38
- trace << definition.to_rule
39
- end
40
-
13
+ super(*args, &block)
41
14
  self
42
15
  end
43
16
  end
@@ -35,7 +35,7 @@ module Dry
35
35
  #
36
36
  # @api public
37
37
  def value(*args, **opts, &block)
38
- extract_type_spec(*args) do |*predicates|
38
+ extract_type_spec(*args) do |*predicates, type_spec:|
39
39
  super(*predicates, **opts, &block)
40
40
  end
41
41
  end
@@ -48,8 +48,8 @@ module Dry
48
48
  #
49
49
  # @api public
50
50
  def filled(*args, **opts, &block)
51
- extract_type_spec(*args) do |*predicates|
52
- super(*predicates, **opts, &block)
51
+ extract_type_spec(*args) do |*predicates, type_spec:|
52
+ super(*predicates, type_spec: type_spec, **opts, &block)
53
53
  end
54
54
  end
55
55
 
@@ -61,7 +61,7 @@ module Dry
61
61
  #
62
62
  # @api public
63
63
  def maybe(*args, **opts, &block)
64
- extract_type_spec(*args, nullable: true) do |*predicates|
64
+ extract_type_spec(*args, nullable: true) do |*predicates, type_spec:|
65
65
  append_macro(Macros::Maybe) do |macro|
66
66
  macro.call(*predicates, **opts, &block)
67
67
  end
@@ -104,7 +104,7 @@ module Dry
104
104
  def extract_type_spec(*args, nullable: false)
105
105
  type_spec = args[0]
106
106
 
107
- if type_spec.kind_of?(Schema::Processor) || type_spec.is_a?(Symbol) && type_spec.to_s.end_with?(QUESTION_MARK)
107
+ if type_spec.kind_of?(Dry::Schema::Processor) || type_spec.is_a?(Symbol) && type_spec.to_s.end_with?(QUESTION_MARK)
108
108
  type_spec = nil
109
109
  end
110
110
 
@@ -129,7 +129,7 @@ module Dry
129
129
  end
130
130
  end
131
131
 
132
- yield(*predicates)
132
+ yield(*predicates, type_spec: !type_spec.nil?)
133
133
  end
134
134
  end
135
135
  end
@@ -0,0 +1,44 @@
1
+ require 'dry/schema/macros/value'
2
+
3
+ module Dry
4
+ module Schema
5
+ module Macros
6
+ # Macro used to specify a nested schema
7
+ #
8
+ # @api public
9
+ class Schema < Value
10
+ # @api private
11
+ def call(*args, &block)
12
+ super(*args) unless args.empty?
13
+
14
+ if block
15
+ definition = schema_dsl.new(&block)
16
+
17
+ parent_type = schema_dsl.types[name]
18
+ definition_schema = definition.type_schema
19
+
20
+ schema_type =
21
+ if parent_type.respond_to?(:of)
22
+ parent_type.of(definition_schema)
23
+ else
24
+ definition_schema
25
+ end
26
+
27
+ final_type =
28
+ if schema_dsl.maybe?(parent_type)
29
+ schema_type.optional
30
+ else
31
+ schema_type
32
+ end
33
+
34
+ schema_dsl.set_type(name, final_type)
35
+
36
+ trace << definition.to_rule
37
+ end
38
+
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -9,6 +9,15 @@ module Dry
9
9
  class Value < DSL
10
10
  # @api private
11
11
  def call(*predicates, **opts, &block)
12
+ schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
13
+
14
+ if schema
15
+ current_type = schema_dsl.types[name]
16
+ updated_type = current_type.respond_to?(:of) ? current_type.of(schema.type_schema) : schema.type_schema
17
+
18
+ schema_dsl.set_type(name, updated_type)
19
+ end
20
+
12
21
  trace.evaluate(*predicates, **opts, &block)
13
22
 
14
23
  trace.append(new(chain: false).instance_exec(&block)) if block
@@ -14,6 +14,8 @@ module Dry
14
14
  #
15
15
  # @api public
16
16
  class Or
17
+ include Enumerable
18
+
17
19
  # @api private
18
20
  attr_reader :left
19
21
 
@@ -34,16 +36,21 @@ module Dry
34
36
  @path = left.path
35
37
  end
36
38
 
37
- # @api private
38
- def hint?
39
- false
40
- end
41
-
42
39
  # Return a string representation of the message
43
40
  #
44
41
  # @api public
45
42
  def to_s
46
- [left, right].uniq.join(" #{messages[:or].()} ")
43
+ uniq.join(" #{messages[:or].()} ")
44
+ end
45
+
46
+ # @api private
47
+ def each(&block)
48
+ to_a.each(&block)
49
+ end
50
+
51
+ # @api private
52
+ def to_a
53
+ [left, right]
47
54
  end
48
55
  end
49
56
 
@@ -75,29 +82,10 @@ module Dry
75
82
  text
76
83
  end
77
84
 
78
- # @api private
79
- def hint?
80
- false
81
- end
82
-
83
85
  # @api private
84
86
  def eql?(other)
85
87
  other.is_a?(String) ? text == other : super
86
88
  end
87
89
  end
88
-
89
- # A hint message sub-type
90
- #
91
- # @api private
92
- class Hint < Message
93
- def self.[](predicate, path, text, options)
94
- Hint.new(predicate, path, text, options)
95
- end
96
-
97
- # @api private
98
- def hint?
99
- true
100
- end
101
- end
102
90
  end
103
91
  end
@@ -19,9 +19,8 @@ module Dry
19
19
  @messages = messages
20
20
  @options = options
21
21
  @full = @options.fetch(:full, false)
22
- @hints = @options.fetch(:hints, true)
23
- @locale = @options.fetch(:locale, messages.default_locale)
24
- @default_lookup_options = { locale: locale }
22
+ @locale = @options[:locale]
23
+ @default_lookup_options = @locale ? { locale: locale } : EMPTY_HASH
25
24
  end
26
25
 
27
26
  # @api private
@@ -29,11 +28,6 @@ module Dry
29
28
  @full
30
29
  end
31
30
 
32
- # @api private
33
- def hints?
34
- @hints
35
- end
36
-
37
31
  # @api private
38
32
  def with(new_options)
39
33
  return self if new_options.empty?
@@ -42,40 +36,35 @@ module Dry
42
36
 
43
37
  # @api private
44
38
  def call(ast)
45
- MessageSet[ast.map { |node| visit(node) }, failures: options.fetch(:failures, true)]
39
+ current_messages = EMPTY_ARRAY.dup
40
+ compiled_messages = ast.map { |node| visit(node, EMPTY_OPTS.dup(current_messages)) }
41
+
42
+ MessageSet[compiled_messages, failures: options.fetch(:failures, true)]
46
43
  end
47
44
 
48
45
  # @api private
49
- def visit(node, *args)
50
- __send__(:"visit_#{node[0]}", node[1], *args)
46
+ def visit(node, opts = EMPTY_OPTS.dup)
47
+ __send__(:"visit_#{node[0]}", node[1], opts)
51
48
  end
52
49
 
53
50
  # @api private
54
- def visit_failure(node, opts = EMPTY_OPTS.dup)
51
+ def visit_failure(node, opts)
55
52
  rule, other = node
56
53
  visit(other, opts.(rule: rule))
57
54
  end
58
55
 
59
56
  # @api private
60
- def visit_hint(node, opts = EMPTY_OPTS.dup)
61
- if hints?
62
- visit(node, opts.(message_type: :hint))
63
- end
64
- end
65
-
66
- # @api private
67
- def visit_each(node, opts = EMPTY_OPTS.dup)
68
- # TODO: we can still generate a hint for elements here!
69
- []
57
+ def visit_hint(node, opts)
58
+ nil
70
59
  end
71
60
 
72
61
  # @api private
73
- def visit_not(node, opts = EMPTY_OPTS.dup)
62
+ def visit_not(node, opts)
74
63
  visit(node, opts.(not: true))
75
64
  end
76
65
 
77
66
  # @api private
78
- def visit_and(node, opts = EMPTY_OPTS.dup)
67
+ def visit_and(node, opts)
79
68
  left, right = node.map { |n| visit(n, opts) }
80
69
 
81
70
  if right
@@ -86,7 +75,7 @@ module Dry
86
75
  end
87
76
 
88
77
  # @api private
89
- def visit_or(node, opts = EMPTY_OPTS.dup)
78
+ def visit_or(node, opts)
90
79
  left, right = node.map { |n| visit(n, opts) }
91
80
 
92
81
  if [left, right].flatten.map(&:path).uniq.size == 1
@@ -99,7 +88,14 @@ module Dry
99
88
  end
100
89
 
101
90
  # @api private
102
- def visit_predicate(node, base_opts = EMPTY_OPTS.dup)
91
+ def visit_namespace(node, opts)
92
+ ns, rest = node
93
+ self.class.new(messages.namespaced(ns), options).visit(rest, opts)
94
+ end
95
+
96
+ # @api private
97
+ def visit_predicate(node, opts)
98
+ base_opts = opts.dup
103
99
  predicate, args = node
104
100
 
105
101
  *arg_vals, val = args.map(&:last)
@@ -121,9 +117,7 @@ module Dry
121
117
 
122
118
  text = message_text(rule, template, tokens, options)
123
119
 
124
- message_class = options[:message_type] == :hint ? Hint : Message
125
-
126
- message_class[
120
+ message_type(options)[
127
121
  predicate, path, text,
128
122
  args: arg_vals,
129
123
  input: input,
@@ -132,13 +126,18 @@ module Dry
132
126
  end
133
127
 
134
128
  # @api private
135
- def visit_key(node, opts = EMPTY_OPTS.dup)
129
+ def message_type(*)
130
+ Message
131
+ end
132
+
133
+ # @api private
134
+ def visit_key(node, opts)
136
135
  name, other = node
137
136
  visit(other, opts.(path: name))
138
137
  end
139
138
 
140
139
  # @api private
141
- def visit_set(node, opts = EMPTY_OPTS.dup)
140
+ def visit_set(node, opts)
142
141
  node.map { |el| visit(el, opts) }
143
142
  end
144
143
 
@@ -149,7 +148,7 @@ module Dry
149
148
  end
150
149
 
151
150
  # @api private
152
- def visit_xor(node, opts = EMPTY_OPTS.dup)
151
+ def visit_xor(node, opts)
153
152
  left, right = node
154
153
  [visit(left, opts), visit(right, opts)].uniq
155
154
  end
@@ -1,3 +1,6 @@
1
+ require 'dry/schema/constants'
2
+ require 'dry/schema/message'
3
+
1
4
  module Dry
2
5
  module Schema
3
6
  # @api private
@@ -12,6 +15,7 @@ module Dry
12
15
  opts[:path] = EMPTY_ARRAY
13
16
  opts[:rule] = nil
14
17
  opts[:message_type] = :failure
18
+ opts[:current_messages] = EMPTY_ARRAY.dup
15
19
  opts
16
20
  end
17
21
 
@@ -24,6 +28,28 @@ module Dry
24
28
  def call(other)
25
29
  merge(other.update(path: [*path, *other[:path]]))
26
30
  end
31
+
32
+ def dup(current_messages = EMPTY_ARRAY.dup)
33
+ opts = super()
34
+ opts[:current_messages] = current_messages
35
+ opts
36
+ end
37
+
38
+ def key_failure?(path)
39
+ failures.any? { |f| f.path == path && f.predicate.equal?(:key?) }
40
+ end
41
+
42
+ def failures
43
+ current_messages.reject(&:hint?)
44
+ end
45
+
46
+ def hints
47
+ current_messages.select(&:hint?)
48
+ end
49
+
50
+ def current_messages
51
+ self[:current_messages]
52
+ end
27
53
  end
28
54
  end
29
55
  end