kronk 1.5.4 → 1.6.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.
@@ -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