kronk 1.8.5 → 1.8.6

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.
@@ -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