dry-schema 0.1.0 → 1.0.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +166 -0
  3. data/LICENSE +1 -1
  4. data/README.md +13 -4
  5. data/config/errors.yml +59 -54
  6. data/lib/dry/schema/compiler.rb +24 -0
  7. data/lib/dry/schema/config.rb +23 -27
  8. data/lib/dry/schema/constants.rb +27 -3
  9. data/lib/dry/schema/dsl.rb +36 -49
  10. data/lib/dry/schema/extensions/hints/compiler_methods.rb +19 -0
  11. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +84 -0
  12. data/lib/dry/schema/extensions/hints/message_set_methods.rb +44 -0
  13. data/lib/dry/schema/extensions/hints/result_methods.rb +47 -0
  14. data/lib/dry/schema/extensions/hints.rb +56 -0
  15. data/lib/dry/schema/extensions/monads.rb +15 -4
  16. data/lib/dry/schema/extensions.rb +6 -0
  17. data/lib/dry/schema/json.rb +4 -2
  18. data/lib/dry/schema/key.rb +8 -10
  19. data/lib/dry/schema/key_coercer.rb +6 -2
  20. data/lib/dry/schema/key_map.rb +5 -4
  21. data/lib/dry/schema/macros/array.rb +39 -0
  22. data/lib/dry/schema/macros/core.rb +2 -0
  23. data/lib/dry/schema/macros/dsl.rb +149 -4
  24. data/lib/dry/schema/macros/each.rb +12 -1
  25. data/lib/dry/schema/macros/filled.rb +14 -7
  26. data/lib/dry/schema/macros/hash.rb +6 -32
  27. data/lib/dry/schema/macros/key.rb +38 -60
  28. data/lib/dry/schema/macros/maybe.rb +5 -3
  29. data/lib/dry/schema/macros/optional.rb +8 -1
  30. data/lib/dry/schema/macros/required.rb +3 -1
  31. data/lib/dry/schema/macros/schema.rb +46 -0
  32. data/lib/dry/schema/macros/value.rb +18 -2
  33. data/lib/dry/schema/macros.rb +4 -0
  34. data/lib/dry/schema/message/or.rb +54 -0
  35. data/lib/dry/schema/message.rb +56 -72
  36. data/lib/dry/schema/message_compiler/visitor_opts.rb +28 -1
  37. data/lib/dry/schema/message_compiler.rb +85 -74
  38. data/lib/dry/schema/message_set.rb +82 -50
  39. data/lib/dry/schema/messages/abstract.rb +84 -44
  40. data/lib/dry/schema/messages/i18n.rb +45 -9
  41. data/lib/dry/schema/messages/namespaced.rb +22 -5
  42. data/lib/dry/schema/messages/template.rb +68 -0
  43. data/lib/dry/schema/messages/yaml.rb +76 -22
  44. data/lib/dry/schema/messages.rb +19 -24
  45. data/lib/dry/schema/namespaced_rule.rb +37 -0
  46. data/lib/dry/schema/params.rb +4 -2
  47. data/lib/dry/schema/path.rb +102 -0
  48. data/lib/dry/schema/predicate.rb +15 -0
  49. data/lib/dry/schema/predicate_inferrer.rb +116 -23
  50. data/lib/dry/schema/predicate_registry.rb +2 -0
  51. data/lib/dry/schema/processor.rb +70 -39
  52. data/lib/dry/schema/result.rb +16 -31
  53. data/lib/dry/schema/rule_applier.rb +10 -3
  54. data/lib/dry/schema/trace.rb +16 -3
  55. data/lib/dry/schema/type_registry.rb +5 -3
  56. data/lib/dry/schema/types.rb +3 -1
  57. data/lib/dry/schema/value_coercer.rb +10 -4
  58. data/lib/dry/schema/version.rb +3 -1
  59. data/lib/dry/schema.rb +39 -13
  60. data/lib/dry-schema.rb +2 -0
  61. metadata +43 -47
@@ -1,102 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/initializer'
1
4
  require 'dry/equalizer'
2
5
 
6
+ require 'dry/schema/path'
7
+ require 'dry/schema/message/or'
8
+
3
9
  module Dry
4
10
  module Schema
5
11
  # Message objects used by message sets
6
12
  #
7
13
  # @api public
8
14
  class Message
9
- include Dry::Equalizer(:predicate, :path, :text, :options)
15
+ include Dry::Equalizer(:text, :path, :predicate, :input)
10
16
 
11
- attr_reader :predicate, :path, :text, :rule, :args, :options
12
-
13
- # A message sub-type used by OR operations
14
- #
15
- # @api public
16
- class Or
17
- # @api private
18
- attr_reader :left
17
+ extend Dry::Initializer
19
18
 
20
- # @api private
21
- attr_reader :right
19
+ # @!attribute [r] text
20
+ # Message text representation created from a localized template
21
+ # @return [String]
22
+ option :text
22
23
 
23
- # @api private
24
- attr_reader :path
24
+ # @!attribute [r] path
25
+ # Path to the value
26
+ # @return [String]
27
+ option :path
25
28
 
26
- # @api private
27
- attr_reader :messages
29
+ # @!attribute [r] predicate
30
+ # Predicate identifier that was used to produce a message
31
+ # @return [Symbol]
32
+ option :predicate
28
33
 
29
- # @api private
30
- def initialize(left, right, messages)
31
- @left = left
32
- @right = right
33
- @messages = messages
34
- @path = left.path
35
- end
34
+ # @!attribute [r] args
35
+ # Optional list of arguments used by the predicate
36
+ # @return [Array]
37
+ option :args, default: proc { EMPTY_ARRAY }
36
38
 
37
- # @api private
38
- def hint?
39
- false
40
- end
39
+ # @!attribute [r] input
40
+ # The input value
41
+ # @return [Object]
42
+ option :input
41
43
 
42
- # Return a string representation of the message
43
- #
44
- # @api public
45
- def to_s
46
- [left, right].uniq.join(" #{messages[:or]} ")
47
- end
48
- end
44
+ # @!attribute [r] meta
45
+ # Arbitrary meta data
46
+ # @return [Hash]
47
+ option :meta, optional: true, default: proc { EMPTY_HASH }
49
48
 
50
- # Build a new message object
49
+ # Dump the message to a representation suitable for the message set hash
51
50
  #
52
- # @api private
53
- def self.[](predicate, path, text, options)
54
- Message.new(predicate, path, text, options)
55
- end
56
-
57
- # @api private
58
- def initialize(predicate, path, text, options)
59
- @predicate = predicate
60
- @path = path.dup
61
- @text = text
62
- @options = options
63
- @rule = options[:rule]
64
- @args = options[:args] || EMPTY_ARRAY
65
-
66
- if predicate == :key?
67
- @path << rule
68
- end
69
- end
70
-
71
- # Return a string representation of the message
51
+ # @return [String,Hash]
72
52
  #
73
53
  # @api public
74
- def to_s
75
- text
76
- end
77
-
78
- # @api private
79
- def hint?
80
- false
54
+ def dump
55
+ @dump ||= meta.empty? ? text : { text: text, **meta }
81
56
  end
57
+ alias to_s dump
82
58
 
59
+ # See if another message is the same
60
+ #
61
+ # If a string is passed, it will be compared with the text
62
+ #
63
+ # @param [Message,String]
64
+ #
65
+ # @return [Boolean]
66
+ #
83
67
  # @api private
84
68
  def eql?(other)
85
69
  other.is_a?(String) ? text == other : super
86
70
  end
87
- 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
71
 
72
+ # See which message is higher in the hierarchy
73
+ #
97
74
  # @api private
98
- def hint?
99
- true
75
+ def <=>(other)
76
+ l_path = Path[path]
77
+ r_path = Path[other.path]
78
+
79
+ unless l_path.same_root?(r_path)
80
+ raise ArgumentError, 'Cannot compare messages from different root paths'
81
+ end
82
+
83
+ l_path <=> r_path
100
84
  end
101
85
  end
102
86
  end
@@ -1,3 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/constants'
4
+ require 'dry/schema/message'
5
+
1
6
  module Dry
2
7
  module Schema
3
8
  # @api private
@@ -10,8 +15,8 @@ module Dry
10
15
  def self.new
11
16
  opts = super
12
17
  opts[:path] = EMPTY_ARRAY
13
- opts[:rule] = nil
14
18
  opts[:message_type] = :failure
19
+ opts[:current_messages] = EMPTY_ARRAY.dup
15
20
  opts
16
21
  end
17
22
 
@@ -24,6 +29,28 @@ module Dry
24
29
  def call(other)
25
30
  merge(other.update(path: [*path, *other[:path]]))
26
31
  end
32
+
33
+ def dup(current_messages = EMPTY_ARRAY.dup)
34
+ opts = super()
35
+ opts[:current_messages] = current_messages
36
+ opts
37
+ end
38
+
39
+ def key_failure?(path)
40
+ failures.any? { |f| f.path == path && f.predicate.equal?(:key?) }
41
+ end
42
+
43
+ def failures
44
+ current_messages.reject(&:hint?)
45
+ end
46
+
47
+ def hints
48
+ current_messages.select(&:hint?)
49
+ end
50
+
51
+ def current_messages
52
+ self[:current_messages]
53
+ end
27
54
  end
28
55
  end
29
56
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/initializer'
4
+
1
5
  require 'dry/schema/constants'
2
6
  require 'dry/schema/message'
3
7
  require 'dry/schema/message_set'
@@ -9,73 +13,84 @@ module Dry
9
13
  #
10
14
  # @api private
11
15
  class MessageCompiler
12
- attr_reader :messages, :options, :locale, :default_lookup_options
16
+ extend Dry::Initializer
17
+
18
+ resolve_key_predicate = proc { |node, opts|
19
+ *arg_vals, val = node.map(&:last)
20
+ [[*opts.path, arg_vals[0]], arg_vals[1..arg_vals.size - 1], val]
21
+ }
22
+
23
+ resolve_predicate = proc { |node, opts|
24
+ [Array(opts.path), *node.map(&:last)]
25
+ }
26
+
27
+ DEFAULT_PREDICATE_RESOLVERS = Hash
28
+ .new(resolve_predicate).update(key?: resolve_key_predicate).freeze
13
29
 
14
30
  EMPTY_OPTS = VisitorOpts.new
15
- LIST_SEPARATOR = ', '.freeze
31
+ EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
16
32
 
17
- # @api private
18
- def initialize(messages, options = {})
19
- @messages = messages
20
- @options = options
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 }
25
- end
33
+ param :messages
26
34
 
27
- # @api private
28
- def full?
29
- @full
30
- end
35
+ option :full, default: -> { false }
36
+ option :locale, default: -> { :en }
37
+ option :predicate_resolvers, default: -> { DEFAULT_PREDICATE_RESOLVERS }
38
+
39
+ attr_reader :options
40
+
41
+ attr_reader :default_lookup_options
31
42
 
32
43
  # @api private
33
- def hints?
34
- @hints
44
+ def initialize(messages, options = EMPTY_HASH)
45
+ super
46
+ @options = options
47
+ @default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
35
48
  end
36
49
 
37
50
  # @api private
38
51
  def with(new_options)
39
52
  return self if new_options.empty?
40
- self.class.new(messages, options.merge(new_options))
53
+
54
+ updated_opts = options.merge(new_options)
55
+
56
+ return self if updated_opts.eql?(options)
57
+
58
+ self.class.new(messages, updated_opts)
41
59
  end
42
60
 
43
61
  # @api private
44
62
  def call(ast)
45
- MessageSet[ast.map { |node| visit(node) }, failures: options.fetch(:failures, true)]
63
+ return EMPTY_MESSAGE_SET if ast.empty?
64
+
65
+ current_messages = EMPTY_ARRAY.dup
66
+ compiled_messages = ast.map { |node| visit(node, EMPTY_OPTS.dup(current_messages)) }
67
+
68
+ MessageSet[compiled_messages, failures: options.fetch(:failures, true)]
46
69
  end
47
70
 
48
71
  # @api private
49
- def visit(node, *args)
50
- __send__(:"visit_#{node[0]}", node[1], *args)
72
+ def visit(node, opts = EMPTY_OPTS.dup)
73
+ __send__(:"visit_#{node[0]}", node[1], opts)
51
74
  end
52
75
 
53
76
  # @api private
54
- def visit_failure(node, opts = EMPTY_OPTS.dup)
77
+ def visit_failure(node, opts)
55
78
  rule, other = node
56
79
  visit(other, opts.(rule: rule))
57
80
  end
58
81
 
59
82
  # @api private
60
- def visit_hint(node, opts = EMPTY_OPTS.dup)
61
- if hints?
62
- visit(node, opts.(message_type: :hint))
63
- end
83
+ def visit_hint(*)
84
+ nil
64
85
  end
65
86
 
66
87
  # @api private
67
- def visit_each(node, opts = EMPTY_OPTS.dup)
68
- # TODO: we can still generate a hint for elements here!
69
- []
70
- end
71
-
72
- # @api private
73
- def visit_not(node, opts = EMPTY_OPTS.dup)
88
+ def visit_not(node, opts)
74
89
  visit(node, opts.(not: true))
75
90
  end
76
91
 
77
92
  # @api private
78
- def visit_and(node, opts = EMPTY_OPTS.dup)
93
+ def visit_and(node, opts)
79
94
  left, right = node.map { |n| visit(n, opts) }
80
95
 
81
96
  if right
@@ -86,59 +101,57 @@ module Dry
86
101
  end
87
102
 
88
103
  # @api private
89
- def visit_or(node, opts = EMPTY_OPTS.dup)
104
+ def visit_or(node, opts)
90
105
  left, right = node.map { |n| visit(n, opts) }
91
106
 
92
107
  if [left, right].flatten.map(&:path).uniq.size == 1
93
- Message::Or.new(left, right, -> k { messages[k, default_lookup_options] })
108
+ Message::Or.new(left, right, proc { |k| messages.translate(k, default_lookup_options) })
94
109
  elsif right.is_a?(Array)
95
110
  right
96
111
  else
97
- [left, right]
112
+ [left, right].flatten.max
98
113
  end
99
114
  end
100
115
 
101
116
  # @api private
102
- def visit_predicate(node, base_opts = EMPTY_OPTS.dup)
117
+ def visit_namespace(node, opts)
118
+ ns, rest = node
119
+ self.class.new(messages.namespaced(ns), options).visit(rest, opts)
120
+ end
121
+
122
+ # @api private
123
+ def visit_predicate(node, opts)
103
124
  predicate, args = node
104
125
 
105
- *arg_vals, val = args.map(&:last)
106
126
  tokens = message_tokens(args)
127
+ path, *arg_vals, input = predicate_resolvers[predicate].(args, opts)
107
128
 
108
- input = val != Undefined ? val : nil
109
-
110
- options = base_opts.update(lookup_options(arg_vals: arg_vals, input: input))
111
- msg_opts = options.update(tokens)
112
-
113
- rule = msg_opts[:rule]
114
- path = msg_opts[:path]
129
+ options = opts.dup.update(
130
+ path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
131
+ ).to_h
115
132
 
116
- template = messages[rule] || messages[predicate, msg_opts]
133
+ template, meta = messages[predicate, options] || raise(MissingMessageError, path)
117
134
 
118
- unless template
119
- raise MissingMessageError, "message for #{predicate} was not found"
120
- end
121
-
122
- text = message_text(rule, template, tokens, options)
135
+ text = message_text(template, tokens, options)
123
136
 
124
- message_class = options[:message_type] == :hint ? Hint : Message
137
+ message_type(options).new(
138
+ text: text, path: path, predicate: predicate, args: arg_vals, input: input, meta: meta
139
+ )
140
+ end
125
141
 
126
- message_class[
127
- predicate, path, text,
128
- args: arg_vals,
129
- input: input,
130
- rule: rule || msg_opts[:name]
131
- ]
142
+ # @api private
143
+ def message_type(*)
144
+ Message
132
145
  end
133
146
 
134
147
  # @api private
135
- def visit_key(node, opts = EMPTY_OPTS.dup)
148
+ def visit_key(node, opts)
136
149
  name, other = node
137
150
  visit(other, opts.(path: name))
138
151
  end
139
152
 
140
153
  # @api private
141
- def visit_set(node, opts = EMPTY_OPTS.dup)
154
+ def visit_set(node, opts)
142
155
  node.map { |el| visit(el, opts) }
143
156
  end
144
157
 
@@ -149,34 +162,32 @@ module Dry
149
162
  end
150
163
 
151
164
  # @api private
152
- def visit_xor(node, opts = EMPTY_OPTS.dup)
165
+ def visit_xor(node, opts)
153
166
  left, right = node
154
167
  [visit(left, opts), visit(right, opts)].uniq
155
168
  end
156
169
 
157
170
  # @api private
158
- def lookup_options(arg_vals: [], input: nil)
171
+ def lookup_options(arg_vals:, input:)
159
172
  default_lookup_options.merge(
160
173
  arg_type: arg_vals.size == 1 && arg_vals[0].class,
161
- val_type: input.class
174
+ val_type: input.equal?(Undefined) ? NilClass : input.class
162
175
  )
163
176
  end
164
177
 
165
178
  # @api private
166
- def message_text(rule, template, tokens, opts)
167
- text = template % tokens
179
+ def message_text(template, tokens, options)
180
+ text = template[template.data(tokens)]
168
181
 
169
- if full?
170
- rule_name = rule ? (messages.rule(rule, opts) || rule) : (opts[:name] || opts[:path].last)
171
- "#{rule_name} #{text}"
172
- else
173
- text
174
- end
182
+ return text unless full
183
+
184
+ rule = options[:path]
185
+ "#{messages.rule(rule, options) || rule} #{text}"
175
186
  end
176
187
 
177
188
  # @api private
178
189
  def message_tokens(args)
179
- args.each_with_object({}) { |arg, hash|
190
+ args.each_with_object({}) do |arg, hash|
180
191
  case arg[1]
181
192
  when Array
182
193
  hash[arg[0]] = arg[1].join(LIST_SEPARATOR)
@@ -186,7 +197,7 @@ module Dry
186
197
  else
187
198
  hash[arg[0]] = arg[1]
188
199
  end
189
- }
200
+ end
190
201
  end
191
202
  end
192
203
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
4
+
1
5
  module Dry
2
6
  module Schema
3
7
  # A set of messages used to generate errors
@@ -7,15 +11,23 @@ module Dry
7
11
  # @api public
8
12
  class MessageSet
9
13
  include Enumerable
10
-
11
- HINT_EXCLUSION = %i(
12
- key? filled? nil? bool?
13
- str? int? float? decimal?
14
- date? date_time? time? hash?
15
- array? format?
16
- ).freeze
17
-
18
- attr_reader :messages, :failures, :hints, :paths, :placeholders, :options
14
+ include Dry::Equalizer(:messages, :options)
15
+
16
+ # A list of compiled message objects
17
+ #
18
+ # @return [Array<Message>]
19
+ attr_reader :messages
20
+
21
+ # An internal hash that is filled in with dumped messages
22
+ # when a message set is coerced to a hash
23
+ #
24
+ # @return [Hash<Symbol=>[Array,Hash]>]
25
+ attr_reader :placeholders
26
+
27
+ # Options hash
28
+ #
29
+ # @return [Hash]
30
+ attr_reader :options
19
31
 
20
32
  # @api private
21
33
  def self.[](messages, options = EMPTY_HASH)
@@ -25,91 +37,111 @@ module Dry
25
37
  # @api private
26
38
  def initialize(messages, options = EMPTY_HASH)
27
39
  @messages = messages
28
- @hints = messages.select(&:hint?)
29
- @failures = messages - hints
30
- @paths = failures.map(&:path).uniq
31
40
  @options = options
32
-
33
- initialize_hints!
34
41
  initialize_placeholders!
35
42
  end
36
43
 
44
+ # Iterate over messages
45
+ #
46
+ # @example
47
+ # result.errors.each do |message|
48
+ # puts message.text
49
+ # end
50
+ #
51
+ # @return [Array]
52
+ #
37
53
  # @api public
38
54
  def each(&block)
55
+ return self if empty?
39
56
  return to_enum unless block
57
+
40
58
  messages.each(&block)
41
59
  end
42
60
 
61
+ # Dump message set to a hash
62
+ #
63
+ # @return [Hash<Symbol=>Array<String>>]
64
+ #
43
65
  # @api public
44
66
  def to_h
45
- failures? ? messages_map : hints_map
67
+ @to_h ||= messages_map
46
68
  end
47
69
  alias_method :to_hash, :to_h
48
- alias_method :dump, :to_h
49
70
 
50
- # @api private
51
- def failures?
52
- options[:failures].equal?(true)
71
+ # Get a list of message texts for the given key
72
+ #
73
+ # @param [Symbol] key
74
+ #
75
+ # @return [Array<String>]
76
+ #
77
+ # @api public
78
+ def [](key)
79
+ to_h[key]
53
80
  end
54
81
 
55
- # @api private
56
- def empty?
57
- messages.empty?
82
+ # Get a list of message texts for the given key
83
+ #
84
+ # @param [Symbol] key
85
+ #
86
+ # @return [Array<String>]
87
+ #
88
+ # @raise KeyError
89
+ #
90
+ # @api public
91
+ def fetch(key)
92
+ self[key] || raise(KeyError, "+#{key}+ message was not found")
58
93
  end
59
94
 
60
- private
95
+ # Check if a message set is empty
96
+ #
97
+ # @return [Boolean]
98
+ #
99
+ # @api public
100
+ def empty?
101
+ @empty ||= messages.empty?
102
+ end
61
103
 
62
104
  # @api private
63
- def messages_map
64
- failures.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
65
- node = path.reduce(hash) { |a, e| a[e] }
66
-
67
- msgs.each do |msg|
68
- node << msg
69
- end
70
-
71
- msg_hints = hint_groups[path]
72
- node.concat(msg_hints) if msg_hints
73
-
74
- node.map!(&:to_s)
75
-
76
- hash
77
- end
105
+ def freeze
106
+ to_h
107
+ empty?
108
+ super
78
109
  end
79
110
 
111
+ private
112
+
80
113
  # @api private
81
- def hints_map
82
- hints.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
114
+ def messages_map(messages = self.messages)
115
+ return EMPTY_HASH if empty?
116
+
117
+ messages.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
83
118
  node = path.reduce(hash) { |a, e| a[e] }
84
119
 
85
120
  msgs.each do |msg|
86
121
  node << msg
87
122
  end
88
123
 
89
- node.map!(&:to_s)
124
+ node.map!(&:dump)
90
125
 
91
126
  hash
92
127
  end
93
128
  end
94
129
 
95
130
  # @api private
96
- def hint_groups
97
- @hint_groups ||= hints.group_by(&:path)
98
- end
99
-
100
- # @api private
101
- def initialize_hints!
102
- hints.reject! { |hint| HINT_EXCLUSION.include?(hint.predicate) }
131
+ def paths
132
+ @paths ||= messages.map(&:path).uniq
103
133
  end
104
134
 
105
135
  # @api private
106
136
  def initialize_placeholders!
107
- @placeholders = paths.reduce({}) do |hash, path|
137
+ return @placeholders = EMPTY_HASH if empty?
138
+
139
+ @placeholders = paths.reduce(EMPTY_HASH.dup) do |hash, path|
108
140
  curr_idx = 0
109
141
  last_idx = path.size - 1
110
142
  node = hash
111
143
 
112
- while curr_idx <= last_idx do
144
+ while curr_idx <= last_idx
113
145
  key = path[curr_idx]
114
146
  node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
115
147
  curr_idx += 1