kronk 1.5.4 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +14 -0
- data/Manifest.txt +8 -2
- data/README.rdoc +6 -0
- data/TODO.rdoc +12 -0
- data/lib/kronk.rb +6 -2
- data/lib/kronk/cmd.rb +18 -2
- data/lib/kronk/constants.rb +1 -0
- data/lib/kronk/data_renderer.rb +95 -22
- data/lib/kronk/diff.rb +18 -64
- data/lib/kronk/diff/ascii_format.rb +11 -0
- data/lib/kronk/diff/color_format.rb +14 -2
- data/lib/kronk/diff/output.rb +155 -0
- data/lib/kronk/path.rb +48 -153
- data/lib/kronk/path/matcher.rb +189 -0
- data/lib/kronk/path/path_match.rb +74 -0
- data/lib/kronk/path/transaction.rb +157 -47
- data/lib/kronk/player/benchmark.rb +2 -1
- data/lib/kronk/player/suite.rb +8 -0
- data/lib/kronk/response.rb +7 -6
- data/test/test_cmd.rb +29 -8
- data/test/test_data_string.rb +58 -0
- data/test/test_diff.rb +137 -36
- data/test/test_helper.rb +2 -0
- data/test/test_kronk.rb +19 -3
- data/test/test_path.rb +87 -170
- data/test/test_path_match.rb +60 -0
- data/test/test_path_matcher.rb +329 -0
- data/test/test_response.rb +10 -10
- data/test/test_transaction.rb +132 -3
- metadata +82 -75
@@ -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
|
45
|
-
@
|
46
|
-
|
47
|
-
|
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 =
|
70
|
-
new_data =
|
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.
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
98
|
-
|
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
|
-
|
103
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
121
|
+
def transaction_move data, match_target_hash # :nodoc:
|
122
|
+
return data if match_target_hash.empty?
|
123
|
+
path_val_hash = {}
|
111
124
|
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
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
|
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
|
178
|
+
yield new_curr_data, curr_data, key, path if block_given?
|
151
179
|
|
152
180
|
else
|
153
|
-
|
154
|
-
new_curr_data[key]
|
181
|
+
if create_empty
|
182
|
+
new_curr_data[key] ||= Hash.new
|
155
183
|
|
156
|
-
|
157
|
-
new_curr_data[key] =
|
158
|
-
|
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
|
-
@
|
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
|