kronk 1.8.5 → 1.8.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,193 +0,0 @@
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 Kronk::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 = /(^|[^#{Kronk::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 ||= Kronk::Path::Match.new
94
- path = Kronk::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{(^|[^#{Kronk::Path::RECH}])(\*+\?*)}, '\1*'
173
- str.gsub! %r{(^|[^#{Kronk::Path::RECH}])\*+}, '\1*'
174
-
175
- str = Regexp.escape str
176
-
177
- # Remove escaping from special path characters
178
- str.gsub! %r{#{Kronk::Path::RECH}([#{PATH_CHARS}])}, '\1'
179
- str.gsub! %r{#{Kronk::Path::RECH}([#{RESC_CHARS}])}, '\1'
180
- str.gsub! %r{(^|[^#{Kronk::Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
181
- str.gsub! %r{(^|[^\.#{Kronk::Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
182
-
183
- Regexp.new "\\A(?:#{str})\\Z", @regex_opts
184
-
185
- else
186
- str.gsub %r{#{Kronk::Path::RECH}([^#{Kronk::Path::RECH}]|$)}, '\1'
187
- end
188
-
189
- else
190
- str
191
- end
192
- end
193
- end
@@ -1,341 +0,0 @@
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 Kronk::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 = Kronk::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 = Kronk::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
- Kronk::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