dry-schema 0.1.1 → 0.2.0

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