kronk 1.5.4 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
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
+
35
+ def initialize opts={}
36
+ @regex_opts = opts[:regex_opts]
37
+ @recursive = !!opts[:recursive]
38
+
39
+ @key = nil
40
+ @key = parse_node opts[:key] if
41
+ opts[:key] && !opts[:key].to_s.empty?
42
+
43
+ @value = nil
44
+ @value = parse_node opts[:value] if
45
+ opts[:value] && !opts[:value].to_s.empty?
46
+ end
47
+
48
+
49
+ def == other # :nodoc:
50
+ self.class == other.class &&
51
+ @key == other.key &&
52
+ @value == other.value &&
53
+ @regex_opts == other.regex_opts
54
+ end
55
+
56
+
57
+ ##
58
+ # Universal iterator for Hash and Array like objects.
59
+ # The data argument must either respond to both :each_with_index
60
+ # and :length, or respond to :each yielding a key/value pair.
61
+
62
+ def each_data_item data, &block
63
+ if data.respond_to?(:has_key?) && data.respond_to?(:each)
64
+ data.each(&block)
65
+
66
+ elsif data.respond_to?(:each_with_index) && data.respond_to?(:length)
67
+ # We need to iterate through the array this way
68
+ # in case items in it get deleted.
69
+ (data.length - 1).downto(0) do |i|
70
+ block.call i, data[i]
71
+ end
72
+ end
73
+ end
74
+
75
+
76
+ ##
77
+ # Finds data with the given key and value matcher, optionally recursively.
78
+ # Yields data, key and path Array when block is given.
79
+ # Returns an Array of path arrays.
80
+
81
+ def find_in data, path=nil, &block
82
+ return [] unless Array === data || Hash === data
83
+
84
+ paths = []
85
+ path ||= Kronk::Path::PathMatch.new
86
+ path = Kronk::Path::PathMatch.new path if path.class == Array
87
+
88
+ each_data_item data do |key, value|
89
+ c_path = path.dup << key
90
+
91
+ found, kmatch = match_node(@key, key) if @key
92
+ found, vmatch = match_node(@value, value) if @value && (!@key || found)
93
+
94
+ if found
95
+ c_path.matches.concat kmatch.to_a
96
+ c_path.matches.concat vmatch.to_a
97
+
98
+ yield data, key, c_path if block_given?
99
+ paths << c_path
100
+ end
101
+
102
+ paths.concat \
103
+ find_in(data[key], c_path, &block) if @recursive
104
+ end
105
+
106
+ paths
107
+ end
108
+
109
+
110
+ ##
111
+ # Check if data key or value is a match for nested data searches.
112
+ # Returns an array with a boolean expressing if the value matched the node,
113
+ # and the matches found.
114
+
115
+ def match_node node, value
116
+ return if ANY_VALUE != node &&
117
+ (Array === value || Hash === value)
118
+
119
+ if node.class == value.class
120
+ node == value
121
+
122
+ elsif Regexp === node
123
+ match = node.match value.to_s
124
+ return false unless match
125
+ match = match.size > 1 ? match[1..-1] : match.to_a
126
+ [true, match]
127
+
128
+ elsif Range === node
129
+ stat = node.include? value.to_i
130
+ match = [value.to_i] if stat
131
+ [stat, match]
132
+
133
+ elsif ANY_VALUE == node
134
+ [true, [value]]
135
+
136
+ else
137
+ value.to_s == node.to_s
138
+ end
139
+ end
140
+
141
+
142
+ ##
143
+ # Decide whether to make path item matcher a regex, range, array, or string.
144
+
145
+ def parse_node str
146
+ case str
147
+ when nil, ANYVAL_MATCHER
148
+ ANY_VALUE
149
+
150
+ when RANGE_MATCHER
151
+ Range.new $1.to_i, $3.to_i, ($2 == "...")
152
+
153
+ when ILEN_MATCHER
154
+ Range.new $1.to_i, ($1.to_i + $2.to_i), true
155
+
156
+ when String
157
+ if @regex_opts || str =~ PATH_CHAR_MATCHER
158
+
159
+ # Remove extra suffix characters
160
+ str.gsub! %r{(^|[^#{Kronk::Path::RECH}])(\*+\?*)}, '\1*'
161
+ str.gsub! %r{(^|[^#{Kronk::Path::RECH}])\*+}, '\1*'
162
+
163
+ str = Regexp.escape str
164
+
165
+ # Remove escaping from special path characters
166
+ str.gsub! %r{#{Kronk::Path::RECH}([#{PATH_CHARS}])}, '\1'
167
+ str.gsub! %r{#{Kronk::Path::RECH}([#{RESC_CHARS}])}, '\1'
168
+ str.gsub! %r{(^|[^#{Kronk::Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
169
+ str.gsub! %r{(^|[^\.#{Kronk::Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'
170
+
171
+ Regexp.new "\\A#{str}\\Z", @regex_opts
172
+
173
+ else
174
+ str.gsub %r{#{Kronk::Path::RECH}([^#{Kronk::Path::RECH}]|$)}, '\1'
175
+ end
176
+
177
+ else
178
+ str
179
+ end
180
+ end
181
+
182
+
183
+ ##
184
+ # Should this matcher try and find a match recursively.
185
+
186
+ def recursive?
187
+ @recursive
188
+ end
189
+ end
@@ -0,0 +1,74 @@
1
+ ##
2
+ # Represents a single match of a relative path against a data set.
3
+
4
+ class Kronk::Path::PathMatch < Array
5
+
6
+ attr_accessor :matches
7
+
8
+ def initialize *args
9
+ @matches = []
10
+ super
11
+ end
12
+
13
+
14
+ def dup # :nodoc:
15
+ path_match = super
16
+ path_match.matches = @matches.dup
17
+ path_match
18
+ end
19
+
20
+
21
+ def append_match_for str, path # :nodoc:
22
+ match = @matches[str.to_i-1]
23
+ if match && !(String === match) && path[-1].empty?
24
+ path[-1] = match
25
+ else
26
+ path[-1] << match.to_s
27
+ end
28
+ end
29
+
30
+
31
+ ##
32
+ # Builds a path array by replacing %n values with matches.
33
+
34
+ def make_path path_map, regex_opts=nil, &block
35
+ path = []
36
+ escape = false
37
+ replace = false
38
+ new_item = true
39
+ rindex = ""
40
+
41
+ path_map.to_s.chars do |chr|
42
+ case chr
43
+ when Kronk::Path::ECH
44
+ escape = true
45
+
46
+ when Kronk::Path::DCH
47
+ new_item = true
48
+
49
+ when Kronk::Path::RCH
50
+ replace = true
51
+ end and next unless escape
52
+
53
+ if replace
54
+ if new_item && !rindex.empty? || chr.to_i.to_s != chr || escape
55
+ append_match_for(rindex, path) unless rindex.empty?
56
+ rindex = ""
57
+ replace = false
58
+ else
59
+ rindex << chr
60
+ end
61
+ end
62
+
63
+ path << "" if new_item
64
+ path.last << chr unless replace
65
+
66
+ new_item = false
67
+ escape = false
68
+ end
69
+
70
+ append_match_for(rindex, path) unless rindex.empty?
71
+
72
+ path
73
+ end
74
+ end
@@ -41,10 +41,16 @@ class Kronk::Path::Transaction
41
41
  # operations on.
42
42
 
43
43
  def initialize data
44
- @data = data
45
- @actions = Hash.new{|h,k| h[k] = []}
46
-
47
- @make_array = []
44
+ @data = data
45
+ @new_data = nil
46
+ @actions = {
47
+ :select => [],
48
+ :delete => [],
49
+ :move => {},
50
+ :map => {}
51
+ }
52
+
53
+ @make_array = {}
48
54
  end
49
55
 
50
56
 
@@ -66,21 +72,26 @@ class Kronk::Path::Transaction
66
72
 
67
73
  def results opts={}
68
74
  new_data = transaction_select @data, *@actions[:select]
69
- new_data = transaction_delete new_data, *@actions[:delete]
70
- new_data = remake_arrays new_data, opts[:keep_indicies]
71
- new_data
75
+ new_data = transaction_map @data, @actions[:map]
76
+ new_data = transaction_move @data, @actions[:move]
77
+ new_data = transaction_delete @data, *@actions[:delete]
78
+ remake_arrays new_data, opts[:keep_indicies]
72
79
  end
73
80
 
74
81
 
75
82
  def remake_arrays new_data, except_modified=false # :nodoc:
76
- @make_array.each do |path_arr|
83
+ remake_paths = @make_array.keys.sort{|p1, p2| p2.length <=> p1.length}
84
+
85
+ remake_paths.each do |path_arr|
77
86
  key = path_arr.last
78
87
  obj = Kronk::Path.data_at_path path_arr[0..-2], new_data
79
88
 
80
89
  next unless obj && Hash === obj[key]
81
- next if except_modified &&
82
- obj[key].length !=
83
- Kronk::Path.data_at_path(path_arr, @data).length
90
+
91
+ if except_modified
92
+ data_at_path = Kronk::Path.data_at_path(path_arr, @data)
93
+ next if !data_at_path || obj[key].length != data_at_path.length
94
+ end
84
95
 
85
96
  obj[key] = hash_to_ary obj[key]
86
97
  end
@@ -94,70 +105,90 @@ class Kronk::Path::Transaction
94
105
 
95
106
 
96
107
  def transaction_select data, *data_paths # :nodoc:
97
- data_paths = data_paths.compact
98
- return data if data_paths.empty?
108
+ transaction data, data_paths, true do |new_curr_data, curr_data, key|
109
+ new_curr_data[key] = curr_data[key]
110
+ end
111
+ end
99
112
 
100
- new_data = Hash.new
101
113
 
102
- data_paths.each do |data_path|
103
- Kronk::Path.find data_path, data do |obj, k, path|
114
+ def transaction_delete data, *data_paths # :nodoc:
115
+ transaction data, data_paths do |new_curr_data, curr_data, key|
116
+ new_curr_data.delete key
117
+ end
118
+ end
104
119
 
105
- curr_data = data
106
- new_curr_data = new_data
107
120
 
108
- path.each_with_index do |key, i|
109
- if i == path.length - 1
110
- new_curr_data[key] = curr_data[key]
121
+ def transaction_move data, match_target_hash # :nodoc:
122
+ return data if match_target_hash.empty?
123
+ path_val_hash = {}
111
124
 
112
- else
113
- new_curr_data[key] ||= Hash.new
125
+ match_target_hash.each do |data_path, path_map|
126
+ transaction data, [data_path] do |new_curr_data, cdata, key, path|
127
+ mapped_path = path.make_path path_map
128
+ path_val_hash[mapped_path] = new_curr_data.delete key
129
+ if @make_array[path]
130
+ @make_array.delete path
131
+ @make_array[mapped_path] = true
132
+ end
133
+ end
134
+ end
114
135
 
115
- # Tag data item for conversion to Array.
116
- # Hashes are used to conserve position of Array elements.
117
- if Array === curr_data[key]
118
- @make_array << path[0..i]
119
- end
136
+ force_assign_paths @new_data, path_val_hash
137
+ end
120
138
 
121
- new_curr_data = new_curr_data[key]
122
- curr_data = curr_data[key]
123
- end
139
+
140
+ def transaction_map data, match_target_hash # :nodoc:
141
+ return data if match_target_hash.empty?
142
+ path_val_hash = {}
143
+
144
+ match_target_hash.each do |data_path, path_map|
145
+ Kronk::Path.find data_path, data do |sdata, key, spath|
146
+ mapped_path = spath.make_path path_map
147
+ path_val_hash[mapped_path] = sdata[key]
148
+ if @make_array[spath]
149
+ @make_array.delete spath
150
+ @make_array[mapped_path] = true
124
151
  end
125
152
  end
126
153
  end
127
154
 
128
- new_data
155
+ force_assign_paths @new_data, path_val_hash
129
156
  end
130
157
 
131
158
 
132
- def transaction_delete data, *data_paths # :nodoc:
159
+ def transaction data, data_paths, create_empty=false # :nodoc:
133
160
  data_paths = data_paths.compact
134
- return data if data_paths.empty?
161
+ return @new_data || data if data_paths.empty?
135
162
 
136
- new_data = data.dup
163
+ @new_data ||= create_empty ? Hash.new : data.dup
137
164
 
138
- if Array === new_data
139
- new_data = ary_to_hash new_data
165
+ if Array === @new_data
166
+ @new_data = ary_to_hash @new_data
140
167
  end
141
168
 
142
169
  data_paths.each do |data_path|
143
170
  Kronk::Path.find data_path, data do |obj, k, path|
144
-
145
171
  curr_data = data
146
- new_curr_data = new_data
172
+ new_curr_data = @new_data
147
173
 
148
174
  path.each_with_index do |key, i|
175
+ break unless new_curr_data
176
+
149
177
  if i == path.length - 1
150
- new_curr_data.delete key
178
+ yield new_curr_data, curr_data, key, path if block_given?
151
179
 
152
180
  else
153
- new_curr_data[key] = new_curr_data[key].dup if
154
- new_curr_data[key] == curr_data[key]
181
+ if create_empty
182
+ new_curr_data[key] ||= Hash.new
155
183
 
156
- if Array === new_curr_data[key]
157
- new_curr_data[key] = ary_to_hash new_curr_data[key]
158
- @make_array << path[0..i]
184
+ elsif new_curr_data[key] == curr_data[key]
185
+ new_curr_data[key] = Array === new_curr_data[key] ?
186
+ ary_to_hash(curr_data[key]) :
187
+ curr_data[key].dup
159
188
  end
160
189
 
190
+ @make_array[path[0..i]] = true if Array === curr_data[key]
191
+
161
192
  new_curr_data = new_curr_data[key]
162
193
  curr_data = curr_data[key]
163
194
  end
@@ -165,7 +196,62 @@ class Kronk::Path::Transaction
165
196
  end
166
197
  end
167
198
 
168
- new_data
199
+ @new_data
200
+ end
201
+
202
+
203
+ def force_assign_paths data, path_val_hash # :nodoc:
204
+ return data if path_val_hash.empty?
205
+ @new_data ||= (data.dup rescue [])
206
+
207
+ path_val_hash.each do |path, value|
208
+ curr_data = data
209
+ new_curr_data = @new_data
210
+ prev_data = nil
211
+ prev_key = nil
212
+ prev_path = []
213
+
214
+ path.each_with_index do |key, i|
215
+ if Array === new_curr_data
216
+ new_curr_data = ary_to_hash new_curr_data
217
+ prev_data[prev_key] = new_curr_data if prev_data
218
+ @new_data = new_curr_data if i == 0
219
+ @make_array[prev_path] = true
220
+ end
221
+
222
+ last = i == path.length - 1
223
+ prev_path = path[0..(i-1)] if i > 0
224
+ curr_path = path[0..i]
225
+ next_key = path[i+1]
226
+
227
+ # new_curr_data is a hash from here on
228
+
229
+ @make_array.delete prev_path unless Integer === key
230
+
231
+ new_curr_data[key] = value and break if last
232
+
233
+ if ary_or_hash?(curr_data) && ary_or_hash?(curr_data[key])
234
+ new_curr_data[key] ||= curr_data[key]
235
+
236
+ elsif !ary_or_hash?(new_curr_data[key])
237
+ new_curr_data[key] = Integer === next_key ? [] : {}
238
+ end
239
+
240
+ @make_array[curr_path] = true if Array === new_curr_data[key]
241
+
242
+ prev_key = key
243
+ prev_data = new_curr_data
244
+ new_curr_data = new_curr_data[key]
245
+ curr_data = ary_or_hash?(curr_data) ? curr_data[key] : nil
246
+ end
247
+ end
248
+
249
+ @new_data
250
+ end
251
+
252
+
253
+ def ary_or_hash? obj # :nodoc:
254
+ Array === obj || Hash === obj
169
255
  end
170
256
 
171
257
 
@@ -185,7 +271,8 @@ class Kronk::Path::Transaction
185
271
  # Clears the queued actions and cache.
186
272
 
187
273
  def clear
188
- @actions.clear
274
+ @new_data = nil
275
+ @actions.each{|k,v| v.clear}
189
276
  @make_array.clear
190
277
  end
191
278
 
@@ -204,4 +291,27 @@ class Kronk::Path::Transaction
204
291
  def delete *paths
205
292
  @actions[:delete].concat paths
206
293
  end
294
+
295
+
296
+ ##
297
+ # Queues path moving for transaction. Moving a path will attempt to
298
+ # keep the original data structure and only affect the given paths.
299
+ # Empty hashes or arrays after a move are deleted.
300
+ # t.move "my/path/1..4/key" => "new_path/%d/key",
301
+ # "other/path/*" => "moved/%d"
302
+
303
+ def move path_maps
304
+ @actions[:move].merge! path_maps
305
+ end
306
+
307
+
308
+ ##
309
+ # Queues path mapping for transaction. Mapping a path will only keep the
310
+ # mapped values, completely replacing the original data structure.
311
+ # t.move "my/path/1..4/key" => "new_path/%d/key",
312
+ # "other/path/*" => "moved/%d"
313
+
314
+ def map path_maps
315
+ @actions[:map].merge! path_maps
316
+ end
207
317
  end