ruby-path 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ ##
2
+ # Data manipulation and retrieval methods for Array and Hash classes.
3
+
4
+ module Path::DataExt
5
+
6
+ ##
7
+ # Checks if the given path exists and returns the first matching path
8
+ # as an array of keys. Returns false if no path is found.
9
+
10
+ def has_path? path
11
+ Path.find path, self do |d,k,p|
12
+ return true
13
+ end
14
+
15
+ false
16
+ end
17
+
18
+
19
+ ##
20
+ # Looks for data at paths matching path. Returns a hash of
21
+ # path array => data value pairs.
22
+ #
23
+ # If given a block will pass the parent data structure, the key
24
+ # or index of the item at given path, and the full path
25
+ # as an array of keys for each found path.
26
+ #
27
+ # data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
28
+ # data.find_data "**/foo" do |parent, key, path|
29
+ # p path
30
+ # p parent[key]
31
+ # puts "---"
32
+ # end
33
+ #
34
+ # # outputs:
35
+ # # [:foo]
36
+ # # "bar"
37
+ # # ---
38
+ # # [:foobar, 2, :foo]
39
+ # # "other bar"
40
+ # # ---
41
+ #
42
+ # # returns:
43
+ # # {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
44
+
45
+ def find_data path, &block
46
+ Path.find path, self, &block
47
+ end
48
+
49
+
50
+ ##
51
+ # Finds and replaces the value of any match with the given new value.
52
+ # Returns true if matches were replaced, otherwise false.
53
+ #
54
+ # data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
55
+ # data.replace_at_path "**=*bar", "BAR"
56
+ # #=> true
57
+ #
58
+ # data
59
+ # #=> {:foo => "BAR", :foobar => [:a, :b, {:foo => "BAR"}, :c]}
60
+ #
61
+ # Note: Specifying a limit will allow only "limit" number of items to be
62
+ # set but may yield unpredictible results for non-ordered Hashes.
63
+ # It's also important to realize that arrays are modified starting with
64
+ # the last index, going down.
65
+
66
+ def replace_at_path path, value, limit=nil
67
+ count = 0
68
+
69
+ Path.find path, self do |data, key, path_arr|
70
+ count = count.next
71
+ data[key] = value
72
+
73
+ return true if limit && count >= limit
74
+ end
75
+
76
+ return count > 0
77
+ end
78
+
79
+
80
+ ##
81
+ # Similar to DataExt#replace_at_path but deletes found items.
82
+ # Returns a hash of path/value pairs of deleted items.
83
+ #
84
+ # data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
85
+ # data.replace_at_path "**=*bar", "BAR"
86
+ # #=> {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
87
+
88
+ def delete_at_path path, limit=nil
89
+ count = 0
90
+ out = {}
91
+
92
+ Path.find path, self do |data, key, path_arr|
93
+ count = count.next
94
+ out[path_arr] = data[key]
95
+
96
+ data.respond_to(:delete_at) ? data.delete_at(key) : data.delete(key)
97
+
98
+ return true if limit && count >= limit
99
+ end
100
+
101
+ return count > 0
102
+ end
103
+ end
104
+
105
+ Array.send :include, Path::DataExt
106
+ Hash.send :include, Path::DataExt
@@ -0,0 +1,130 @@
1
+ ##
2
+ # Represents the single match of a relative path against a data set.
3
+
4
+ class Path::Match < Array
5
+
6
+ attr_accessor :matches, :splat
7
+
8
+ def initialize *args
9
+ @matches = []
10
+ @splat = []
11
+ super
12
+ end
13
+
14
+
15
+ def [] selector
16
+ path_match = super
17
+
18
+ if self.class === path_match
19
+ path_match.matches = @matches.dup
20
+ path_match.splat = @splat.map{|key, sp| [key, sp.dup]}
21
+ end
22
+
23
+ path_match
24
+ end
25
+
26
+
27
+ def append_splat id, key # :nodoc:
28
+ if @splat[-1] && @splat[-1][0] == id
29
+ @splat[-1][1] << key
30
+ else
31
+ @splat << [id, [key]]
32
+ end
33
+ end
34
+
35
+
36
+ def dup # :nodoc:
37
+ path_match = super
38
+ path_match.matches = @matches.dup
39
+ path_match.splat = @splat.map{|key, sp| [key, sp.dup]}
40
+ path_match
41
+ end
42
+
43
+
44
+ def append_match_for str, path # :nodoc:
45
+ match = @matches[str.to_i-1]
46
+ if match && !(String === match) && path[-1].empty?
47
+ path[-1] = match
48
+ else
49
+ path[-1] << match.to_s
50
+ end
51
+ end
52
+
53
+
54
+ ##
55
+ # Builds a path array by replacing %n and %% values with matches and splat.
56
+ #
57
+ # matches = Path.find_in "**/foo=bar", data
58
+ # # [["path", "to", "foo"]]
59
+ #
60
+ # matches.first.make_path "root/%%/foo"
61
+ # # ["root", "path", "to", "foo"]
62
+ #
63
+ # matches = Path.find_in "path/*/(foo)=bar", data
64
+ # # [["path", "to", "foo"]]
65
+ #
66
+ # matches.first.make_path "root/%1/%2"
67
+ # # ["root", "to", "foo"]
68
+
69
+ def make_path path_map, regex_opts=nil, &block
70
+ tmpsplat = @splat.dup
71
+ path = []
72
+ escape = false
73
+ replace = false
74
+ new_item = true
75
+ rindex = ""
76
+
77
+ path_map.to_s.chars do |chr|
78
+ case chr
79
+ when Path::ECH
80
+ escape = true
81
+
82
+ when Path::DCH
83
+ new_item = true
84
+
85
+ when Path::RCH
86
+ if replace
87
+ if rindex.empty?
88
+ unless tmpsplat.empty?
89
+ items = tmpsplat.shift[1].dup
90
+ if new_item
91
+ new_item = false
92
+ else
93
+ path[-1] = path[-1].dup << items.shift
94
+ end
95
+ path.concat items
96
+ end
97
+ replace = false
98
+ else
99
+ append_match_for(rindex, path)
100
+ rindex = ""
101
+ end
102
+
103
+ next
104
+ else
105
+ replace = true
106
+ end
107
+ end and next unless escape
108
+
109
+ if replace
110
+ if new_item && !rindex.empty? || chr.to_i.to_s != chr || escape
111
+ append_match_for(rindex, path) unless rindex.empty?
112
+ rindex = ""
113
+ replace = false
114
+ else
115
+ rindex << chr
116
+ end
117
+ end
118
+
119
+ path << "" if new_item
120
+ path.last << chr unless replace
121
+
122
+ new_item = false
123
+ escape = false
124
+ end
125
+
126
+ append_match_for(rindex, path) unless rindex.empty?
127
+
128
+ path
129
+ end
130
+ end
@@ -0,0 +1,193 @@
1
+ ##
2
+ # Path::Matcher is representation of a single node of a relative path used
3
+ # to find values in a data set.
4
+
5
+ class Path::Matcher
6
+
7
+ # Used as path item value to match any key or value.
8
+ module ANY_VALUE; end
9
+
10
+ # Shortcut characters that require modification before being turned into
11
+ # a matcher.
12
+ SUFF_CHARS = Regexp.escape "*?"
13
+
14
+ # All special path characters.
15
+ PATH_CHARS = Regexp.escape("()|") << SUFF_CHARS
16
+
17
+ # Path chars that get regexp escaped.
18
+ RESC_CHARS = "*?()|/"
19
+
20
+ # Matcher for Range path item.
21
+ RANGE_MATCHER = %r{^(\-?\d+)(\.{2,3})(\-?\d+)$}
22
+
23
+ # Matcher for index,length path item.
24
+ ILEN_MATCHER = %r{^(\-?\d+),(\-?\d+)$}
25
+
26
+ # Matcher allowing any value to be matched.
27
+ ANYVAL_MATCHER = /^(\?*\*+\?*)*$/
28
+
29
+ # Matcher to assert if any unescaped special chars are in a path item.
30
+ PATH_CHAR_MATCHER = /(^|[^#{Path::RECH}])([#{PATH_CHARS}])/
31
+
32
+
33
+ attr_reader :key, :value, :regex_opts
34
+ attr_accessor :recursive
35
+
36
+ ##
37
+ # New instance of Matcher. Options supported:
38
+ # :key:: String - The path item key to match.
39
+ # :value:: String - The path item value to match.
40
+ # :recursive:: Boolean - Look for path item recursively.
41
+ # :regex_opts:: Fixnum - representing the Regexp options.
42
+
43
+ def initialize opts={}
44
+ @regex_opts = opts[:regex_opts]
45
+ @recursive = !!opts[:recursive]
46
+
47
+ @key = nil
48
+ @key = parse_node opts[:key] if
49
+ opts[:key] && !opts[:key].to_s.empty?
50
+
51
+ @value = nil
52
+ @value = parse_node opts[:value] if
53
+ opts[:value] && !opts[:value].to_s.empty?
54
+ end
55
+
56
+
57
+ def == other # :nodoc:
58
+ self.class == other.class &&
59
+ @key == other.key &&
60
+ @value == other.value &&
61
+ @regex_opts == other.regex_opts
62
+ end
63
+
64
+
65
+ ##
66
+ # Universal iterator for Hash and Array like objects.
67
+ # The data argument must either respond to both :each_with_index
68
+ # and :length, or respond to :has_key? and :each yielding a key/value pair.
69
+
70
+ def each_data_item data, &block
71
+ if data.respond_to?(:has_key?) && data.respond_to?(:each)
72
+ data.each(&block)
73
+
74
+ elsif data.respond_to?(:each_with_index) && data.respond_to?(:length)
75
+ # We need to iterate through the array this way
76
+ # in case items in it get deleted.
77
+ (data.length - 1).downto(0) do |i|
78
+ block.call i, data[i]
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ ##
85
+ # Finds data with the given key and value matcher, optionally recursive.
86
+ # Yields data, key and path Array when block is given.
87
+ # Returns an Array of path arrays.
88
+
89
+ def find_in data, path=nil, &block
90
+ return [] unless Array === data || Hash === data
91
+
92
+ paths = []
93
+ path ||= Path::Match.new
94
+ path = Path::Match.new path if path.class == Array
95
+
96
+ each_data_item data do |key, value|
97
+ c_path = path.dup << key
98
+
99
+ found, kmatch = match_node(@key, key) if @key
100
+ found, vmatch = match_node(@value, value) if @value && (!@key || found)
101
+
102
+ c_path.append_splat self, key if @recursive
103
+
104
+ if found
105
+ c_path.matches.concat kmatch.to_a
106
+ c_path.matches.concat vmatch.to_a
107
+
108
+ f_path = c_path.dup
109
+ f_path.splat[-1][-1].pop if @key && !f_path.splat.empty?
110
+
111
+ yield data, key, f_path if block_given?
112
+ paths << f_path
113
+ end
114
+
115
+ paths.concat find_in(data[key], c_path, &block) if @recursive
116
+ end
117
+
118
+ paths
119
+ end
120
+
121
+
122
+ ##
123
+ # Check if data key or value is a match for nested data searches.
124
+ # Returns an array with a boolean expressing if the value matched the node,
125
+ # and the matches found.
126
+
127
+ def match_node node, value
128
+ return if ANY_VALUE != node &&
129
+ (Array === value || Hash === value)
130
+
131
+ if node.class == value.class
132
+ node == value
133
+
134
+ elsif Regexp === node
135
+ match = node.match value.to_s
136
+ return false unless match
137
+ match = match.size > 1 ? match[1..-1] : match.to_a
138
+ [true, match]
139
+
140
+ elsif Range === node
141
+ stat = node.include? value.to_i
142
+ match = [value.to_i] if stat
143
+ [stat, match]
144
+
145
+ elsif ANY_VALUE == node
146
+ [true, [value]]
147
+
148
+ else
149
+ value.to_s == node.to_s
150
+ end
151
+ end
152
+
153
+
154
+ ##
155
+ # Decide whether to make path item matcher a regex, range, array, or string.
156
+
157
+ def parse_node str
158
+ case str
159
+ when nil, ANYVAL_MATCHER
160
+ ANY_VALUE
161
+
162
+ when RANGE_MATCHER
163
+ Range.new $1.to_i, $3.to_i, ($2 == "...")
164
+
165
+ when ILEN_MATCHER
166
+ Range.new $1.to_i, ($1.to_i + $2.to_i), true
167
+
168
+ when String
169
+ if @regex_opts || str =~ PATH_CHAR_MATCHER
170
+
171
+ # Remove extra suffix characters
172
+ str.gsub! %r{(^|[^#{Path::RECH}])(\*+\?*)}, '\1*'
173
+ str.gsub! %r{(^|[^#{Path::RECH}])\*+}, '\1*'
174
+
175
+ str = Regexp.escape str
176
+
177
+ # Remove escaping from special path characters
178
+ str.gsub! %r{#{Path::RECH}([#{PATH_CHARS}])}, '\1'
179
+ str.gsub! %r{#{Path::RECH}([#{RESC_CHARS}])}, '\1'
180
+ str.gsub! %r{(^|[^#{Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
181
+ str.gsub! %r{(^|[^\.#{Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
182
+
183
+ Regexp.new "\\A(?:#{str})\\Z", @regex_opts
184
+
185
+ else
186
+ str.gsub %r{#{Path::RECH}([^#{Path::RECH}]|$)}, '\1'
187
+ end
188
+
189
+ else
190
+ str
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,341 @@
1
+ ##
2
+ # Path Transactions are a convenient way to apply selections and deletions
3
+ # to complex data structures without having to know what state the data will
4
+ # be in after each operation.
5
+ #
6
+ # data = [
7
+ # {:name => "Jamie", :id => "12345"},
8
+ # {:name => "Adam", :id => "54321"},
9
+ # {:name => "Kari", :id => "12345"},
10
+ # {:name => "Grant", :id => "12345"},
11
+ # {:name => "Tory", :id => "12345"},
12
+ # ]
13
+ #
14
+ # # Select all element names, delete the one at index 2,
15
+ # # and move the element with the value "Tory" to the same path but
16
+ # # with the key renamed to "boo"
17
+ # Transaction.run data do |t|
18
+ # t.select "*/name"
19
+ # t.move "**=Tory" => "%%/boo"
20
+ # t.delete "2"
21
+ # end
22
+ #
23
+ # # => [
24
+ # # {:name => "Jamie"},
25
+ # # {:name => "Adam"},
26
+ # # {:name => "Grant"},
27
+ # # {"boo" => "Tory"},
28
+ # # ]
29
+
30
+ class Path::Transaction
31
+
32
+ ##
33
+ # Create new Transaction instance and run it with a block.
34
+ # Equivalent to:
35
+ # Transaction.new(data).run(opts)
36
+
37
+ def self.run data, opts={}, &block
38
+ new(data).run opts, &block
39
+ end
40
+
41
+
42
+ attr_accessor :actions
43
+
44
+ ##
45
+ # Create a new Transaction instance with a the data object to perform
46
+ # operations on.
47
+
48
+ def initialize data
49
+ @data = data
50
+ @new_data = nil
51
+ @actions = []
52
+
53
+ @make_array = {}
54
+ end
55
+
56
+
57
+ ##
58
+ # Run operations as a transaction.
59
+ # See Transaction#results for supported options.
60
+
61
+ def run opts={}, &block
62
+ clear
63
+ yield self if block_given?
64
+ results opts
65
+ end
66
+
67
+
68
+ ##
69
+ # Returns the results of the transaction operations.
70
+ # To keep the original indicies of modified arrays, and return them as hashes,
71
+ # pass the :keep_indicies => true option.
72
+
73
+ def results opts={}
74
+ new_data = @data
75
+
76
+ @actions.each do |type, paths|
77
+ new_data = send("transaction_#{type}", new_data, *paths)
78
+ end
79
+
80
+ remake_arrays new_data, opts[:keep_indicies]
81
+ end
82
+
83
+
84
+ def remake_arrays new_data, except_modified=false # :nodoc:
85
+ remake_paths = @make_array.keys.sort{|p1, p2| p2.length <=> p1.length}
86
+
87
+ remake_paths.each do |path_arr|
88
+ key = path_arr.last
89
+ obj = Path.data_at_path path_arr[0..-2], new_data
90
+
91
+ next unless obj && Hash === obj[key]
92
+
93
+ if except_modified
94
+ data_at_path = Path.data_at_path(path_arr, @data)
95
+ next if !data_at_path || obj[key].length != data_at_path.length
96
+ end
97
+
98
+ obj[key] = hash_to_ary obj[key]
99
+ end
100
+
101
+ new_data = hash_to_ary new_data if
102
+ (remake_paths.last == [] || Array === @data && Hash === new_data) &&
103
+ (!except_modified || @data.length == new_data.length)
104
+
105
+ new_data
106
+ end
107
+
108
+
109
+ def remap_make_arrays new_path, old_path # :nodoc:
110
+ @make_array[new_path] = true and return if @make_array[old_path]
111
+
112
+ @make_array.keys.each do |path|
113
+ if path[0...old_path.length] == old_path
114
+ path[0...old_path.length] = new_path
115
+ end
116
+ end
117
+ end
118
+
119
+
120
+ def transaction_select data, *data_paths # :nodoc:
121
+ return data if data_paths.empty?
122
+
123
+ transaction data, data_paths, true do |sdata, cdata, key, path, tpath|
124
+ sdata[key] = cdata[key]
125
+ end
126
+ end
127
+
128
+
129
+ def transaction_delete data, *data_paths # :nodoc:
130
+ transaction data, data_paths do |new_curr_data, curr_data, key|
131
+ new_curr_data.delete key
132
+ end
133
+ end
134
+
135
+
136
+ def transaction_move data, *path_pairs # :nodoc:
137
+ return data if path_pairs.empty?
138
+ path_val_hash = {}
139
+
140
+ new_data =
141
+ transaction data, path_pairs do |sdata, cdata, key, path, tpath|
142
+ path_val_hash[tpath] = sdata.delete key
143
+ remap_make_arrays(tpath, path)
144
+ end
145
+
146
+ force_assign_paths new_data, path_val_hash
147
+ end
148
+
149
+
150
+ def transaction_map data, *path_pairs # :nodoc:
151
+ return data if path_pairs.empty?
152
+ path_val_hash = {}
153
+
154
+ transaction data, path_pairs do |sdata, cdata, key, path, tpath|
155
+ tpath ||= path
156
+ path_val_hash[tpath] = sdata.delete key
157
+ remap_make_arrays(tpath, path)
158
+ end
159
+
160
+ force_assign_paths [], path_val_hash
161
+ end
162
+
163
+
164
+ def transaction data, data_paths, create_empty=false # :nodoc:
165
+ data_paths = data_paths.compact
166
+ return @new_data || data if data_paths.empty?
167
+
168
+ @new_data = create_empty ? Hash.new : data.dup
169
+
170
+ if Array === @new_data
171
+ @new_data = ary_to_hash @new_data
172
+ end
173
+
174
+ data_paths.each do |data_path|
175
+ # If data_path is an array, the second element is the path where the value
176
+ # should be mapped to.
177
+ data_path, target_path = data_path
178
+
179
+ Path.find data_path, data do |obj, k, path|
180
+ curr_data = data
181
+ new_curr_data = @new_data
182
+
183
+ path.each_with_index do |key, i|
184
+ break unless new_curr_data
185
+
186
+ if i == path.length - 1
187
+ tpath = path.make_path target_path if target_path
188
+ yield new_curr_data, curr_data, key, path, tpath if block_given?
189
+
190
+ else
191
+ if create_empty
192
+ new_curr_data[key] ||= Hash.new
193
+
194
+ elsif new_curr_data[key] == curr_data[key]
195
+ new_curr_data[key] = Array === new_curr_data[key] ?
196
+ ary_to_hash(curr_data[key]) :
197
+ curr_data[key].dup
198
+ end
199
+
200
+ @make_array[path[0..i]] = true if Array === curr_data[key]
201
+
202
+ new_curr_data = new_curr_data[key]
203
+ curr_data = curr_data[key]
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ @new_data
210
+ end
211
+
212
+
213
+ def force_assign_paths data, path_val_hash # :nodoc:
214
+ return data if path_val_hash.empty?
215
+ @new_data = (data.dup rescue [])
216
+
217
+ path_val_hash.each do |path, value|
218
+ curr_data = data
219
+ new_curr_data = @new_data
220
+ prev_data = nil
221
+ prev_key = nil
222
+ prev_path = []
223
+
224
+ path.each_with_index do |key, i|
225
+ if Array === new_curr_data
226
+ new_curr_data = ary_to_hash new_curr_data
227
+ prev_data[prev_key] = new_curr_data if prev_data
228
+ @new_data = new_curr_data if i == 0
229
+ @make_array[prev_path] = true if i == 0
230
+ end
231
+
232
+ last = i == path.length - 1
233
+ prev_path = path[0..(i-1)] if i > 0
234
+ curr_path = path[0..i]
235
+ next_key = path[i+1]
236
+
237
+ # new_curr_data is a hash from here on
238
+
239
+ @make_array.delete prev_path unless is_integer?(key)
240
+
241
+ new_curr_data[key] = value and break if last
242
+
243
+ if ary_or_hash?(curr_data) && child_ary_or_hash?(curr_data, key)
244
+ new_curr_data[key] ||= curr_data[key]
245
+
246
+ elsif !ary_or_hash?(new_curr_data[key])
247
+ new_curr_data[key] = is_integer?(next_key) ? [] : {}
248
+ end
249
+
250
+ @make_array[curr_path] = true if Array === new_curr_data[key]
251
+
252
+ prev_key = key
253
+ prev_data = new_curr_data
254
+ new_curr_data = new_curr_data[key]
255
+ curr_data = curr_data[key] if ary_or_hash?(curr_data) rescue nil
256
+ end
257
+ end
258
+
259
+ @new_data
260
+ end
261
+
262
+
263
+ def is_integer? item # :nodoc:
264
+ item.to_s.to_i.to_s == item.to_s
265
+ end
266
+
267
+
268
+ def ary_or_hash? obj # :nodoc:
269
+ Array === obj || Hash === obj
270
+ end
271
+
272
+
273
+ def child_ary_or_hash? obj, key
274
+ ary_or_hash?(obj[key]) rescue false
275
+ end
276
+
277
+
278
+ def ary_to_hash ary # :nodoc:
279
+ hash = {}
280
+ ary.each_with_index{|val, i| hash[i] = val}
281
+ hash
282
+ end
283
+
284
+
285
+ def hash_to_ary hash # :nodoc:
286
+ hash.keys.sort.map{|k| hash[k] }
287
+ end
288
+
289
+
290
+ ##
291
+ # Clears the queued actions and cache.
292
+
293
+ def clear
294
+ @new_data = nil
295
+ @actions.clear
296
+ @make_array.clear
297
+ end
298
+
299
+
300
+ ##
301
+ # Queues path selects for transaction.
302
+
303
+ def select *paths
304
+ if @actions.last && @actions.last[0] == :select
305
+ @actions.last[1].concat paths
306
+ else
307
+ @actions << [:select, paths]
308
+ end
309
+ end
310
+
311
+
312
+ ##
313
+ # Queues path deletes for transaction.
314
+
315
+ def delete *paths
316
+ @actions << [:delete, paths]
317
+ end
318
+
319
+
320
+ ##
321
+ # Queues path moving for transaction. Moving a path will attempt to
322
+ # keep the original data structure and only affect the given paths.
323
+ # Empty hashes or arrays after a move are deleted.
324
+ # t.move "my/path/1..4/key" => "new_path/%1/key",
325
+ # "other/path/*" => "moved/%1"
326
+
327
+ def move path_maps
328
+ @actions << [:move, Array(path_maps)]
329
+ end
330
+
331
+
332
+ ##
333
+ # Queues path mapping for transaction. Mapping a path will only keep the
334
+ # mapped values, completely replacing the original data structure.
335
+ # t.map "my/path/1..4/key" => "new_path/%1/key",
336
+ # "other/path/*" => "moved/%1"
337
+
338
+ def map path_maps
339
+ @actions << [:map, Array(path_maps)]
340
+ end
341
+ end