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