dry-schema 1.6.2 → 1.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +4 -3
- data/dry-schema.gemspec +16 -14
- data/lib/dry/schema/compiler.rb +1 -1
- data/lib/dry/schema/config.rb +9 -9
- data/lib/dry/schema/dsl.rb +7 -4
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
- data/lib/dry/schema/extensions/hints.rb +11 -9
- data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
- data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
- data/lib/dry/schema/extensions/json_schema.rb +29 -0
- data/lib/dry/schema/extensions/struct.rb +1 -1
- data/lib/dry/schema/extensions.rb +4 -0
- data/lib/dry/schema/key.rb +75 -70
- data/lib/dry/schema/key_coercer.rb +2 -2
- data/lib/dry/schema/key_validator.rb +46 -20
- data/lib/dry/schema/macros/array.rb +4 -0
- data/lib/dry/schema/macros/core.rb +1 -1
- data/lib/dry/schema/macros/dsl.rb +17 -15
- data/lib/dry/schema/macros/hash.rb +1 -1
- data/lib/dry/schema/macros/key.rb +2 -2
- data/lib/dry/schema/macros/schema.rb +2 -0
- data/lib/dry/schema/macros/value.rb +13 -1
- data/lib/dry/schema/message/or/multi_path.rb +7 -5
- data/lib/dry/schema/message_compiler.rb +13 -10
- data/lib/dry/schema/messages/abstract.rb +9 -9
- data/lib/dry/schema/messages/i18n.rb +98 -96
- data/lib/dry/schema/messages/namespaced.rb +1 -0
- data/lib/dry/schema/messages/yaml.rb +165 -151
- data/lib/dry/schema/path.rb +10 -60
- data/lib/dry/schema/predicate.rb +2 -2
- data/lib/dry/schema/predicate_inferrer.rb +2 -0
- data/lib/dry/schema/primitive_inferrer.rb +2 -0
- data/lib/dry/schema/processor.rb +6 -6
- data/lib/dry/schema/processor_steps.rb +7 -3
- data/lib/dry/schema/result.rb +38 -31
- data/lib/dry/schema/step.rb +14 -33
- data/lib/dry/schema/trace.rb +5 -1
- data/lib/dry/schema/type_registry.rb +1 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +11 -8
data/lib/dry/schema/key.rb
CHANGED
@@ -85,94 +85,99 @@ module Dry
|
|
85
85
|
def coerced_name
|
86
86
|
@__coerced_name__ ||= coercer[name]
|
87
87
|
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# A specialized key type which handles nested hashes
|
91
|
-
#
|
92
|
-
# @api private
|
93
|
-
class Key::Hash < Key
|
94
|
-
include Dry.Equalizer(:name, :members, :coercer)
|
95
88
|
|
89
|
+
# A specialized key type which handles nested hashes
|
90
|
+
#
|
96
91
|
# @api private
|
97
|
-
|
92
|
+
class Hash < self
|
93
|
+
include Dry.Equalizer(:name, :members, :coercer)
|
98
94
|
|
99
|
-
|
100
|
-
|
101
|
-
super(id, **opts)
|
102
|
-
@members = members
|
103
|
-
end
|
95
|
+
# @api private
|
96
|
+
attr_reader :members
|
104
97
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
98
|
+
# @api private
|
99
|
+
def initialize(id, members:, **opts)
|
100
|
+
super(id, **opts)
|
101
|
+
@members = members
|
102
|
+
end
|
109
103
|
|
110
|
-
|
111
|
-
read(source)
|
112
|
-
|
113
|
-
|
114
|
-
end
|
104
|
+
# @api private
|
105
|
+
def read(source)
|
106
|
+
super if source.is_a?(::Hash)
|
107
|
+
end
|
115
108
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
109
|
+
def write(source, target)
|
110
|
+
read(source) { |value|
|
111
|
+
target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
|
112
|
+
}
|
113
|
+
end
|
120
114
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
115
|
+
# @api private
|
116
|
+
def coercible(&coercer)
|
117
|
+
new(coercer: coercer, members: members.coercible(&coercer))
|
118
|
+
end
|
125
119
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
120
|
+
# @api private
|
121
|
+
def stringified
|
122
|
+
new(name: name.to_s, members: members.stringified)
|
123
|
+
end
|
130
124
|
|
131
|
-
|
132
|
-
|
133
|
-
|
125
|
+
# @api private
|
126
|
+
def to_dot_notation
|
127
|
+
[name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
def dump
|
132
|
+
{name => members.map(&:dump)}
|
133
|
+
end
|
134
134
|
end
|
135
|
-
end
|
136
135
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
136
|
+
# A specialized key type which handles nested arrays
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
class Array < self
|
140
|
+
include Dry.Equalizer(:name, :member, :coercer)
|
142
141
|
|
143
|
-
|
142
|
+
attr_reader :member
|
144
143
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
144
|
+
# @api private
|
145
|
+
def initialize(id, member:, **opts)
|
146
|
+
super(id, **opts)
|
147
|
+
@member = member
|
148
|
+
end
|
150
149
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
150
|
+
# @api private
|
151
|
+
def write(source, target)
|
152
|
+
read(source) { |value|
|
153
|
+
target[coerced_name] =
|
154
|
+
if value.is_a?(::Array)
|
155
|
+
value.map { |el| el.is_a?(::Hash) ? member.write(el) : el }
|
156
|
+
else
|
157
|
+
value
|
158
|
+
end
|
159
|
+
}
|
160
|
+
end
|
157
161
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
+
# @api private
|
163
|
+
def coercible(&coercer)
|
164
|
+
new(coercer: coercer, member: member.coercible(&coercer))
|
165
|
+
end
|
162
166
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
+
# @api private
|
168
|
+
def stringified
|
169
|
+
new(name: name.to_s, member: member.stringified)
|
170
|
+
end
|
167
171
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
+
# @api private
|
173
|
+
def to_dot_notation
|
174
|
+
[:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
|
175
|
+
end
|
172
176
|
|
173
|
-
|
174
|
-
|
175
|
-
|
177
|
+
# @api private
|
178
|
+
def dump
|
179
|
+
[name, member.dump]
|
180
|
+
end
|
176
181
|
end
|
177
182
|
end
|
178
183
|
end
|
@@ -24,17 +24,7 @@ module Dry
|
|
24
24
|
key_paths = key_map.to_dot_notation
|
25
25
|
|
26
26
|
input_paths.each do |path|
|
27
|
-
error_path =
|
28
|
-
if path[INDEX_REGEX]
|
29
|
-
key = path.gsub(INDEX_REGEX, BRACKETS)
|
30
|
-
|
31
|
-
if key_paths.none? { |key_path| key_path.include?(key) }
|
32
|
-
arr = path.gsub(INDEX_REGEX) { |m| ".#{m[1]}" }
|
33
|
-
arr.split(DOT).map { |s| DIGIT_REGEX.match?(s) ? s.to_i : s.to_sym }
|
34
|
-
end
|
35
|
-
elsif key_paths.none? { |key_path| key_path.include?(path) }
|
36
|
-
path
|
37
|
-
end
|
27
|
+
error_path = validate_path(key_paths, path)
|
38
28
|
|
39
29
|
next unless error_path
|
40
30
|
|
@@ -46,22 +36,58 @@ module Dry
|
|
46
36
|
|
47
37
|
private
|
48
38
|
|
39
|
+
# @api private
|
40
|
+
def validate_path(key_paths, path)
|
41
|
+
if path[INDEX_REGEX]
|
42
|
+
key = path.gsub(INDEX_REGEX, BRACKETS)
|
43
|
+
|
44
|
+
if key_paths.none? { paths_match?(key, _1) }
|
45
|
+
arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
|
46
|
+
arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
|
47
|
+
end
|
48
|
+
elsif key_paths.none? { paths_match?(path, _1) }
|
49
|
+
path
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def paths_match?(input_path, key_path)
|
55
|
+
residue = key_path.sub(input_path, "")
|
56
|
+
residue.empty? || residue.start_with?(DOT, BRACKETS)
|
57
|
+
end
|
58
|
+
|
49
59
|
# @api private
|
50
60
|
def key_paths(hash)
|
51
|
-
hash.flat_map { |key,
|
52
|
-
case
|
53
|
-
when Hash
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
hash.flat_map { |key, value|
|
62
|
+
case value
|
63
|
+
when ::Hash
|
64
|
+
if value.empty?
|
65
|
+
[key.to_s]
|
66
|
+
else
|
67
|
+
[key].product(key_paths(hash[key])).map { _1.join(DOT) }
|
68
|
+
end
|
69
|
+
when ::Array
|
70
|
+
hashes_or_arrays = hashes_or_arrays(value)
|
71
|
+
|
72
|
+
if hashes_or_arrays.empty?
|
73
|
+
[key.to_s]
|
74
|
+
else
|
75
|
+
hashes_or_arrays.flat_map.with_index { |el, idx|
|
76
|
+
key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
|
77
|
+
}
|
78
|
+
end
|
60
79
|
else
|
61
80
|
key.to_s
|
62
81
|
end
|
63
82
|
}
|
64
83
|
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
def hashes_or_arrays(xs)
|
87
|
+
xs.select { |x|
|
88
|
+
(x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
|
89
|
+
}
|
90
|
+
end
|
65
91
|
end
|
66
92
|
end
|
67
93
|
end
|
@@ -10,6 +10,8 @@ module Dry
|
|
10
10
|
# @api private
|
11
11
|
class Array < DSL
|
12
12
|
# @api private
|
13
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
14
|
+
# rubocop: disable Metrics/AbcSize
|
13
15
|
def value(*args, **opts, &block)
|
14
16
|
type(:array)
|
15
17
|
|
@@ -40,6 +42,8 @@ module Dry
|
|
40
42
|
|
41
43
|
self
|
42
44
|
end
|
45
|
+
# rubocop: enable Metrics/AbcSize
|
46
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
43
47
|
|
44
48
|
# @api private
|
45
49
|
def to_ast(*)
|
@@ -58,9 +58,9 @@ module Dry
|
|
58
58
|
# @return [Macros::Core]
|
59
59
|
#
|
60
60
|
# @api public
|
61
|
-
def value(
|
61
|
+
def value(...)
|
62
62
|
append_macro(Macros::Value) do |macro|
|
63
|
-
macro.call(
|
63
|
+
macro.call(...)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
ruby2_keywords :value if respond_to?(:ruby2_keywords, true)
|
@@ -76,9 +76,9 @@ module Dry
|
|
76
76
|
# @return [Macros::Core]
|
77
77
|
#
|
78
78
|
# @api public
|
79
|
-
def filled(
|
79
|
+
def filled(...)
|
80
80
|
append_macro(Macros::Filled) do |macro|
|
81
|
-
macro.call(
|
81
|
+
macro.call(...)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
ruby2_keywords :filled if respond_to?(:ruby2_keywords, true)
|
@@ -97,9 +97,9 @@ module Dry
|
|
97
97
|
# @return [Macros::Core]
|
98
98
|
#
|
99
99
|
# @api public
|
100
|
-
def schema(
|
100
|
+
def schema(...)
|
101
101
|
append_macro(Macros::Schema) do |macro|
|
102
|
-
macro.call(
|
102
|
+
macro.call(...)
|
103
103
|
end
|
104
104
|
end
|
105
105
|
ruby2_keywords :schema if respond_to?(:ruby2_keywords, true)
|
@@ -112,9 +112,9 @@ module Dry
|
|
112
112
|
# end
|
113
113
|
#
|
114
114
|
# @api public
|
115
|
-
def hash(
|
115
|
+
def hash(...)
|
116
116
|
append_macro(Macros::Hash) do |macro|
|
117
|
-
macro.call(
|
117
|
+
macro.call(...)
|
118
118
|
end
|
119
119
|
end
|
120
120
|
ruby2_keywords :hash if respond_to?(:ruby2_keywords, true)
|
@@ -136,9 +136,9 @@ module Dry
|
|
136
136
|
# @return [Macros::Core]
|
137
137
|
#
|
138
138
|
# @api public
|
139
|
-
def each(
|
139
|
+
def each(...)
|
140
140
|
append_macro(Macros::Each) do |macro|
|
141
|
-
macro.value(
|
141
|
+
macro.value(...)
|
142
142
|
end
|
143
143
|
end
|
144
144
|
ruby2_keywords :each if respond_to?(:ruby2_keywords, true)
|
@@ -156,9 +156,9 @@ module Dry
|
|
156
156
|
# @return [Macros::Core]
|
157
157
|
#
|
158
158
|
# @api public
|
159
|
-
def array(
|
159
|
+
def array(...)
|
160
160
|
append_macro(Macros::Array) do |macro|
|
161
|
-
macro.value(
|
161
|
+
macro.value(...)
|
162
162
|
end
|
163
163
|
end
|
164
164
|
ruby2_keywords :array if respond_to?(:ruby2_keywords, true)
|
@@ -200,10 +200,11 @@ module Dry
|
|
200
200
|
end
|
201
201
|
|
202
202
|
# @api private
|
203
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
203
204
|
def extract_type_spec(*args, nullable: false, set_type: true)
|
204
205
|
type_spec = args[0] unless schema_or_predicate?(args[0])
|
205
206
|
|
206
|
-
predicates = Array(type_spec ? args[1
|
207
|
+
predicates = Array(type_spec ? args[1..] : args)
|
207
208
|
type_rule = nil
|
208
209
|
|
209
210
|
if type_spec
|
@@ -228,6 +229,7 @@ module Dry
|
|
228
229
|
yield(*predicates, type_spec: type_spec, type_rule: nil)
|
229
230
|
end
|
230
231
|
end
|
232
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
231
233
|
|
232
234
|
# @api private
|
233
235
|
def resolve_type(type_spec, nullable)
|
@@ -243,8 +245,8 @@ module Dry
|
|
243
245
|
# @api private
|
244
246
|
def schema_or_predicate?(arg)
|
245
247
|
arg.is_a?(Dry::Schema::Processor) ||
|
246
|
-
arg.is_a?(Symbol) &&
|
247
|
-
arg.to_s.end_with?(QUESTION_MARK)
|
248
|
+
(arg.is_a?(Symbol) &&
|
249
|
+
arg.to_s.end_with?(QUESTION_MARK))
|
248
250
|
end
|
249
251
|
end
|
250
252
|
end
|
@@ -26,8 +26,8 @@ module Dry
|
|
26
26
|
# @return [Macros::Key]
|
27
27
|
#
|
28
28
|
# @api public
|
29
|
-
def filter(
|
30
|
-
(filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(
|
29
|
+
def filter(...)
|
30
|
+
(filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(...)
|
31
31
|
self
|
32
32
|
end
|
33
33
|
ruby2_keywords(:filter) if respond_to?(:ruby2_keywords, true)
|
@@ -45,6 +45,7 @@ module Dry
|
|
45
45
|
end
|
46
46
|
|
47
47
|
# @api private
|
48
|
+
# rubocop: disable Metrics/AbcSize
|
48
49
|
def define(*args, &block)
|
49
50
|
definition = schema_dsl.new(path: schema_dsl.path, &block)
|
50
51
|
schema = definition.call
|
@@ -66,6 +67,7 @@ module Dry
|
|
66
67
|
|
67
68
|
schema
|
68
69
|
end
|
70
|
+
# rubocop: enable Metrics/AbcSize
|
69
71
|
|
70
72
|
# @api private
|
71
73
|
def parent_type
|
@@ -11,9 +11,18 @@ module Dry
|
|
11
11
|
# @api private
|
12
12
|
class Value < DSL
|
13
13
|
# @api private
|
14
|
-
|
14
|
+
#
|
15
|
+
# rubocop:disable Metrics/AbcSize
|
16
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
17
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
18
|
+
def call(*args, **opts, &block)
|
19
|
+
types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
|
20
|
+
|
21
|
+
constructor = types.select { |type| type.is_a?(Dry::Types::Constructor) }.reduce(:>>)
|
15
22
|
schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
|
16
23
|
|
24
|
+
schema_dsl.set_type(name, constructor) if constructor
|
25
|
+
|
17
26
|
type_spec = opts[:type_spec]
|
18
27
|
|
19
28
|
if schema
|
@@ -60,6 +69,9 @@ module Dry
|
|
60
69
|
|
61
70
|
self
|
62
71
|
end
|
72
|
+
# rubocop:enable Metrics/AbcSize
|
73
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
74
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
63
75
|
|
64
76
|
# @api private
|
65
77
|
def array_type?(type)
|
@@ -17,17 +17,19 @@ module Dry
|
|
17
17
|
attr_reader :root
|
18
18
|
|
19
19
|
# @api private
|
20
|
-
def initialize(
|
20
|
+
def initialize(...)
|
21
21
|
super
|
22
|
-
|
23
|
-
|
24
|
-
@
|
22
|
+
flat_left = left.flatten
|
23
|
+
flat_right = right.flatten
|
24
|
+
@root = [*flat_left, *flat_right].map(&:_path).reduce(:&)
|
25
|
+
@left = flat_left.map { _1.to_or(root) }
|
26
|
+
@right = flat_right.map { _1.to_or(root) }
|
25
27
|
end
|
26
28
|
|
27
29
|
# @api public
|
28
30
|
def to_h
|
29
31
|
@to_h ||= Path[[*root, :or]].to_h(
|
30
|
-
[
|
32
|
+
[MessageSet.new(left).to_h, MessageSet.new(right).to_h]
|
31
33
|
)
|
32
34
|
end
|
33
35
|
end
|
@@ -30,14 +30,14 @@ module Dry
|
|
30
30
|
|
31
31
|
EMPTY_OPTS = VisitorOpts.new
|
32
32
|
EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
|
33
|
-
FULL_MESSAGE_WHITESPACE = Hash.new(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
FULL_MESSAGE_WHITESPACE = Hash.new(" ").merge(
|
34
|
+
ja: "",
|
35
|
+
zh: "",
|
36
|
+
bn: "",
|
37
|
+
th: "",
|
38
|
+
lo: "",
|
39
|
+
my: ""
|
40
|
+
)
|
41
41
|
|
42
42
|
param :messages
|
43
43
|
|
@@ -206,7 +206,8 @@ module Dry
|
|
206
206
|
return text if !text || !full
|
207
207
|
|
208
208
|
rule = options[:path]
|
209
|
-
[messages.rule(rule, options) || rule,
|
209
|
+
[messages.rule(rule, options) || rule,
|
210
|
+
text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
|
210
211
|
end
|
211
212
|
|
212
213
|
# @api private
|
@@ -228,7 +229,9 @@ module Dry
|
|
228
229
|
# @api private
|
229
230
|
def append_mapped_size_tokens(tokens)
|
230
231
|
# this is a temporary fix for the inconsistency in the "size" errors arguments
|
231
|
-
mapped_hash = tokens.each_with_object({}) { |(k, v), h|
|
232
|
+
mapped_hash = tokens.each_with_object({}) { |(k, v), h|
|
233
|
+
h[k.to_s.gsub("size", "num").to_sym] = v
|
234
|
+
}
|
232
235
|
tokens.merge(mapped_hash)
|
233
236
|
end
|
234
237
|
end
|
@@ -18,13 +18,13 @@ module Dry
|
|
18
18
|
include Dry::Configurable
|
19
19
|
include Dry::Equalizer(:config)
|
20
20
|
|
21
|
-
setting :default_locale
|
22
|
-
setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
|
23
|
-
setting :top_namespace, DEFAULT_MESSAGES_ROOT
|
24
|
-
setting :root, "errors"
|
25
|
-
setting :lookup_options, %i[root predicate path val_type arg_type].freeze
|
21
|
+
setting :default_locale
|
22
|
+
setting :load_paths, default: Set[DEFAULT_MESSAGES_PATH]
|
23
|
+
setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
|
24
|
+
setting :root, default: "errors"
|
25
|
+
setting :lookup_options, default: %i[root predicate path val_type arg_type].freeze
|
26
26
|
|
27
|
-
setting :lookup_paths, [
|
27
|
+
setting :lookup_paths, default: [
|
28
28
|
"%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
|
29
29
|
"%<root>s.rules.%<path>s.%<predicate>s",
|
30
30
|
"%<root>s.%<predicate>s.%<message_type>s",
|
@@ -35,13 +35,13 @@ module Dry
|
|
35
35
|
"%<root>s.%<predicate>s"
|
36
36
|
].freeze
|
37
37
|
|
38
|
-
setting :rule_lookup_paths, ["rules.%<name>s"].freeze
|
38
|
+
setting :rule_lookup_paths, default: ["rules.%<name>s"].freeze
|
39
39
|
|
40
|
-
setting :arg_types, Hash.new { |*| "default" }.update(
|
40
|
+
setting :arg_types, default: Hash.new { |*| "default" }.update(
|
41
41
|
Range => "range"
|
42
42
|
)
|
43
43
|
|
44
|
-
setting :val_types, Hash.new { |*| "default" }.update(
|
44
|
+
setting :val_types, default: Hash.new { |*| "default" }.update(
|
45
45
|
Range => "range",
|
46
46
|
String => "string"
|
47
47
|
)
|