kronk 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +28 -0
- data/Manifest.txt +4 -0
- data/README.rdoc +46 -18
- data/Rakefile +0 -3
- data/bin/kronk +2 -1
- data/lib/kronk.rb +19 -98
- data/lib/kronk/cmd.rb +22 -12
- data/lib/kronk/data_set.rb +55 -228
- data/lib/kronk/diff.rb +65 -4
- data/lib/kronk/path.rb +427 -0
- data/lib/kronk/path/transaction.rb +204 -0
- data/lib/kronk/request.rb +66 -5
- data/lib/kronk/response.rb +1 -1
- data/lib/kronk/test/assertions.rb +4 -8
- data/lib/kronk/test/core_ext.rb +57 -9
- data/lib/kronk/xml_parser.rb +41 -7
- data/script/kronk_completion +3 -2
- data/test/test_core_ext.rb +2 -2
- data/test/test_data_set.rb +4 -297
- data/test/test_diff.rb +12 -12
- data/test/test_kronk.rb +2 -0
- data/test/test_path.rb +370 -0
- data/test/test_request.rb +0 -10
- data/test/test_transaction.rb +330 -0
- metadata +31 -63
- data/.gemtest +0 -0
@@ -0,0 +1,204 @@
|
|
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 but delete the one at index 2
|
15
|
+
# Transaction.run data do |t|
|
16
|
+
# t.select "*/name"
|
17
|
+
# t.delete "2"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # => [
|
21
|
+
# # {:name => "Jamie"},
|
22
|
+
# # {:name => "Adam"},
|
23
|
+
# # {:name => "Grant"},
|
24
|
+
# # {:name => "Tory"},
|
25
|
+
# # ]
|
26
|
+
|
27
|
+
class Kronk::Path::Transaction
|
28
|
+
|
29
|
+
##
|
30
|
+
# Create new Transaction instance and run it with a block.
|
31
|
+
# Equivalent to:
|
32
|
+
# Transaction.new(data).run(opts)
|
33
|
+
|
34
|
+
def self.run data, opts={}, &block
|
35
|
+
new(data).run opts, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
##
|
40
|
+
# Create a new Transaction instance with a the data object to perform
|
41
|
+
# operations on.
|
42
|
+
|
43
|
+
def initialize data
|
44
|
+
@data = data
|
45
|
+
@actions = Hash.new{|h,k| h[k] = []}
|
46
|
+
|
47
|
+
@make_array = []
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
##
|
52
|
+
# Run operations as a transaction.
|
53
|
+
# See Transaction#results for supported options.
|
54
|
+
|
55
|
+
def run opts={}, &block
|
56
|
+
clear
|
57
|
+
yield self if block_given?
|
58
|
+
results opts
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
##
|
63
|
+
# Returns the results of the transaction operations.
|
64
|
+
# To keep the original indicies of modified arrays, and return them as hashes,
|
65
|
+
# pass the :keep_indicies => true option.
|
66
|
+
|
67
|
+
def results opts={}
|
68
|
+
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
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def remake_arrays new_data, except_modified=false # :nodoc:
|
76
|
+
@make_array.each do |path_arr|
|
77
|
+
key = path_arr.last
|
78
|
+
obj = Kronk::Path.data_at_path path_arr[0..-2], new_data
|
79
|
+
|
80
|
+
next unless Hash === obj[key]
|
81
|
+
next if except_modified &&
|
82
|
+
obj[key].length !=
|
83
|
+
Kronk::Path.data_at_path(path_arr, @data).length
|
84
|
+
|
85
|
+
obj[key] = hash_to_ary obj[key]
|
86
|
+
end
|
87
|
+
|
88
|
+
new_data = hash_to_ary new_data if
|
89
|
+
Array === @data && Hash === new_data &&
|
90
|
+
(!except_modified || @data.length == new_data.length)
|
91
|
+
|
92
|
+
new_data
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def transaction_select data, *data_paths # :nodoc:
|
97
|
+
return data if data_paths.empty?
|
98
|
+
|
99
|
+
new_data = Hash.new
|
100
|
+
|
101
|
+
data_paths.each do |data_path|
|
102
|
+
Kronk::Path.find data_path, data do |obj, k, path|
|
103
|
+
|
104
|
+
curr_data = data
|
105
|
+
new_curr_data = new_data
|
106
|
+
|
107
|
+
path.each_with_index do |key, i|
|
108
|
+
if i == path.length - 1
|
109
|
+
new_curr_data[key] = curr_data[key]
|
110
|
+
|
111
|
+
else
|
112
|
+
new_curr_data[key] ||= Hash.new
|
113
|
+
|
114
|
+
# Tag data item for conversion to Array.
|
115
|
+
# Hashes are used to conserve position of Array elements.
|
116
|
+
if Array === curr_data[key]
|
117
|
+
@make_array << path[0..i]
|
118
|
+
end
|
119
|
+
|
120
|
+
new_curr_data = new_curr_data[key]
|
121
|
+
curr_data = curr_data[key]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
new_data
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def transaction_delete data, *data_paths # :nodoc:
|
132
|
+
return data if data_paths.empty?
|
133
|
+
|
134
|
+
new_data = data.dup
|
135
|
+
|
136
|
+
if Array === new_data
|
137
|
+
new_data = ary_to_hash new_data
|
138
|
+
end
|
139
|
+
|
140
|
+
data_paths.each do |data_path|
|
141
|
+
Kronk::Path.find data_path, data do |obj, k, path|
|
142
|
+
|
143
|
+
curr_data = data
|
144
|
+
new_curr_data = new_data
|
145
|
+
|
146
|
+
path.each_with_index do |key, i|
|
147
|
+
if i == path.length - 1
|
148
|
+
new_curr_data.delete key
|
149
|
+
|
150
|
+
else
|
151
|
+
new_curr_data[key] = curr_data[key].dup
|
152
|
+
|
153
|
+
if Array === new_curr_data[key]
|
154
|
+
new_curr_data[key] = ary_to_hash new_curr_data[key]
|
155
|
+
@make_array << path[0..i]
|
156
|
+
end
|
157
|
+
|
158
|
+
new_curr_data = new_curr_data[key]
|
159
|
+
curr_data = curr_data[key]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
new_data
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def ary_to_hash ary # :nodoc:
|
170
|
+
hash = {}
|
171
|
+
ary.each_with_index{|val, i| hash[i] = val}
|
172
|
+
hash
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
def hash_to_ary hash # :nodoc:
|
177
|
+
hash.keys.sort.map{|k| hash[k] }
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
##
|
182
|
+
# Clears the queued actions and cache.
|
183
|
+
|
184
|
+
def clear
|
185
|
+
@actions.clear
|
186
|
+
@make_array.clear
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
##
|
191
|
+
# Queues path selects for transaction.
|
192
|
+
|
193
|
+
def select *paths
|
194
|
+
@actions[:select].concat paths
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
##
|
199
|
+
# Queues path deletes for transaction.
|
200
|
+
|
201
|
+
def delete *paths
|
202
|
+
@actions[:delete].concat paths
|
203
|
+
end
|
204
|
+
end
|
data/lib/kronk/request.rb
CHANGED
@@ -41,7 +41,7 @@ class Kronk
|
|
41
41
|
|
42
42
|
|
43
43
|
##
|
44
|
-
# Returns the value from a url, file, or
|
44
|
+
# Returns the value from a url, file, or IO as a String.
|
45
45
|
# Options supported are:
|
46
46
|
# :data:: Hash/String - the data to pass to the http request
|
47
47
|
# :query:: Hash/String - the data to append to the http request path
|
@@ -55,7 +55,7 @@ class Kronk
|
|
55
55
|
def self.retrieve uri, options={}
|
56
56
|
if IO === uri || StringIO === uri
|
57
57
|
resp = retrieve_io uri, options
|
58
|
-
elsif
|
58
|
+
elsif File.file? uri
|
59
59
|
resp = retrieve_file uri, options
|
60
60
|
else
|
61
61
|
resp = retrieve_uri uri, options
|
@@ -87,9 +87,7 @@ class Kronk
|
|
87
87
|
Kronk::Cmd.verbose "Reading file: #{path}\n"
|
88
88
|
|
89
89
|
options = options.dup
|
90
|
-
|
91
|
-
path = Kronk::DEFAULT_CACHE_FILE if path == :cache
|
92
|
-
resp = nil
|
90
|
+
resp = nil
|
93
91
|
|
94
92
|
File.open(path, "rb") do |file|
|
95
93
|
|
@@ -324,6 +322,69 @@ class Kronk
|
|
324
322
|
end
|
325
323
|
|
326
324
|
|
325
|
+
##
|
326
|
+
# Parses a nested query. Stolen from Rack.
|
327
|
+
|
328
|
+
def self.parse_nested_query qs, d=nil
|
329
|
+
params = {}
|
330
|
+
d ||= "&;"
|
331
|
+
|
332
|
+
(qs || '').split(%r{[#{d}] *}n).each do |p|
|
333
|
+
k, v = CGI.unescape(p).split('=', 2)
|
334
|
+
normalize_params(params, k, v)
|
335
|
+
end
|
336
|
+
|
337
|
+
params
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
##
|
342
|
+
# Stolen from Rack.
|
343
|
+
|
344
|
+
def self.normalize_params params, name, v=nil
|
345
|
+
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
346
|
+
k = $1 || ''
|
347
|
+
after = $' || ''
|
348
|
+
|
349
|
+
return if k.empty?
|
350
|
+
|
351
|
+
if after == ""
|
352
|
+
params[k] = v
|
353
|
+
|
354
|
+
elsif after == "[]"
|
355
|
+
params[k] ||= []
|
356
|
+
raise TypeError,
|
357
|
+
"expected Array (got #{params[k].class.name}) for param `#{k}'" unless
|
358
|
+
params[k].is_a?(Array)
|
359
|
+
|
360
|
+
params[k] << v
|
361
|
+
|
362
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
363
|
+
child_key = $1
|
364
|
+
params[k] ||= []
|
365
|
+
raise TypeError,
|
366
|
+
"expected Array (got #{params[k].class.name}) for param `#{k}'" unless
|
367
|
+
params[k].is_a?(Array)
|
368
|
+
|
369
|
+
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
|
370
|
+
normalize_params(params[k].last, child_key, v)
|
371
|
+
else
|
372
|
+
params[k] << normalize_params({}, child_key, v)
|
373
|
+
end
|
374
|
+
|
375
|
+
else
|
376
|
+
params[k] ||= {}
|
377
|
+
raise TypeError,
|
378
|
+
"expected Hash (got #{params[k].class.name}) for param `#{k}'" unless
|
379
|
+
params[k].is_a?(Hash)
|
380
|
+
|
381
|
+
params[k] = normalize_params(params[k], after, v)
|
382
|
+
end
|
383
|
+
|
384
|
+
return params
|
385
|
+
end
|
386
|
+
|
387
|
+
|
327
388
|
##
|
328
389
|
# Allow any http method to be sent
|
329
390
|
|
data/lib/kronk/response.rb
CHANGED
@@ -12,8 +12,7 @@ class Kronk
|
|
12
12
|
msg ||= "No data found at #{path.inspect} for #{data.inspect}"
|
13
13
|
found = false
|
14
14
|
|
15
|
-
|
16
|
-
data_set.find_data path do |d,k,p|
|
15
|
+
Kronk::Path.find path, data do |d,k,p|
|
17
16
|
found = true
|
18
17
|
break
|
19
18
|
end
|
@@ -30,8 +29,7 @@ class Kronk
|
|
30
29
|
msg ||= "Data found at #{path.inspect} for #{data.inspect}"
|
31
30
|
found = false
|
32
31
|
|
33
|
-
|
34
|
-
data_set.find_data path do |d,k,p|
|
32
|
+
Kronk::Path.find path, data do |d,k,p|
|
35
33
|
found = true
|
36
34
|
break
|
37
35
|
end
|
@@ -49,8 +47,7 @@ class Kronk
|
|
49
47
|
last_data = nil
|
50
48
|
found = false
|
51
49
|
|
52
|
-
|
53
|
-
data_set.find_data path do |d,k,p|
|
50
|
+
Kronk::Path.find path, data do |d,k,p|
|
54
51
|
found = true
|
55
52
|
last_data = d[k]
|
56
53
|
break if d[k] == match
|
@@ -71,8 +68,7 @@ class Kronk
|
|
71
68
|
def assert_data_at_not_equal data, path, match, msg=nil
|
72
69
|
last_data = nil
|
73
70
|
|
74
|
-
|
75
|
-
data_set.find_data path do |d,k,p|
|
71
|
+
Kronk::Path.find path, data do |d,k,p|
|
76
72
|
last_data = d[k]
|
77
73
|
break if d[k] == match
|
78
74
|
end
|
data/lib/kronk/test/core_ext.rb
CHANGED
@@ -9,14 +9,14 @@ class Kronk
|
|
9
9
|
|
10
10
|
##
|
11
11
|
# Checks if the given path exists and returns the first matching path
|
12
|
-
# as an array of keys. Returns
|
12
|
+
# as an array of keys. Returns false if no path is found.
|
13
13
|
|
14
14
|
def has_path? path
|
15
|
-
Kronk::
|
15
|
+
Kronk::Path.find path, self do |d,k,p|
|
16
16
|
return !!p
|
17
17
|
end
|
18
18
|
|
19
|
-
|
19
|
+
false
|
20
20
|
end
|
21
21
|
|
22
22
|
|
@@ -46,15 +46,63 @@ class Kronk
|
|
46
46
|
# # returns:
|
47
47
|
# # {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
|
48
48
|
|
49
|
-
def find_data path
|
50
|
-
|
49
|
+
def find_data path, &block
|
50
|
+
Kronk::Path.find path, self, &block
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
##
|
55
|
+
# Finds and replaces the value of any match with the given new value.
|
56
|
+
# Returns true if matches were replaced, otherwise false.
|
57
|
+
#
|
58
|
+
# data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
59
|
+
# data.replace_at_path "**=*bar", "BAR"
|
60
|
+
# #=> true
|
61
|
+
#
|
62
|
+
# data
|
63
|
+
# #=> {:foo => "BAR", :foobar => [:a, :b, {:foo => "BAR"}, :c]}
|
64
|
+
#
|
65
|
+
# Note: Specifying a limit will allow only "limit" number of items to be
|
66
|
+
# set but may yield unpredictible results for non-ordered Hashes.
|
67
|
+
# It's also important to realize that arrays are modified starting with
|
68
|
+
# the last index, going down.
|
69
|
+
|
70
|
+
def replace_at_path path, value, limit=nil
|
71
|
+
count = 0
|
72
|
+
|
73
|
+
Kronk::Path.find path, self do |data, key, path_arr|
|
74
|
+
count = count.next
|
75
|
+
data[key] = value
|
76
|
+
|
77
|
+
return true if limit && count >= limit
|
78
|
+
end
|
79
|
+
|
80
|
+
return count > 0
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
##
|
85
|
+
# Similar to DataExt#replace_at_path but deletes found items.
|
86
|
+
# Returns a hash of path/value pairs of deleted items.
|
87
|
+
#
|
88
|
+
# data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
89
|
+
# data.replace_at_path "**=*bar", "BAR"
|
90
|
+
# #=> {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
|
91
|
+
|
92
|
+
def delete_at_path path, limit=nil
|
93
|
+
count = 0
|
94
|
+
out = {}
|
95
|
+
|
96
|
+
Kronk::Path.find path, self do |data, key, path_arr|
|
97
|
+
count = count.next
|
98
|
+
out[path_arr] = data[key]
|
99
|
+
|
100
|
+
data.respond_to(:delete_at) ? data.delete_at(key) : data.delete(key)
|
51
101
|
|
52
|
-
|
53
|
-
found[p] = d[k]
|
54
|
-
yield d, k, p if block_given?
|
102
|
+
return true if limit && count >= limit
|
55
103
|
end
|
56
104
|
|
57
|
-
|
105
|
+
return count > 0
|
58
106
|
end
|
59
107
|
end
|
60
108
|
end
|
data/lib/kronk/xml_parser.rb
CHANGED
@@ -6,20 +6,29 @@ class Kronk
|
|
6
6
|
class XMLParser
|
7
7
|
|
8
8
|
##
|
9
|
-
# Load required gems.
|
9
|
+
# Load required gems. Loads Nokogiri. ActiveSupport will attempt to be
|
10
|
+
# loaded if String#pluralize is not defined.
|
10
11
|
|
11
12
|
def self.require_gems
|
12
13
|
require 'nokogiri'
|
13
14
|
|
15
|
+
return if "".respond_to?(:pluralize)
|
16
|
+
|
14
17
|
# Support for new and old versions of ActiveSupport
|
18
|
+
active_support_versions = %w{active_support/inflector activesupport}
|
19
|
+
asupp_i = 0
|
20
|
+
|
15
21
|
begin
|
16
|
-
require
|
22
|
+
require active_support_versions[asupp_i]
|
23
|
+
|
17
24
|
rescue LoadError => e
|
18
|
-
raise unless e.message =~ /--
|
19
|
-
|
25
|
+
raise unless e.message =~ /-- active_?support/
|
26
|
+
asupp_i = asupp_i.next
|
27
|
+
retry if asupp_i < active_support_versions.length
|
20
28
|
end
|
21
29
|
end
|
22
30
|
|
31
|
+
|
23
32
|
##
|
24
33
|
# Takes an xml string and returns a data hash.
|
25
34
|
# Ignores blank spaces between tags.
|
@@ -117,9 +126,34 @@ class Kronk
|
|
117
126
|
n.name
|
118
127
|
end
|
119
128
|
|
120
|
-
names.uniq.length == 1
|
121
|
-
|
122
|
-
|
129
|
+
return false unless names.uniq.length == 1
|
130
|
+
return true if names.length > 1
|
131
|
+
return false unless parent_name
|
132
|
+
|
133
|
+
names.first == parent_name ||
|
134
|
+
names.first.respond_to?(:pluralize) &&
|
135
|
+
names.first.pluralize == parent_name ||
|
136
|
+
pluralize(names.first) == parent_name
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
##
|
141
|
+
# Naïve pluralization used if String#pluralize isn't defined.
|
142
|
+
|
143
|
+
def self.pluralize str
|
144
|
+
case str
|
145
|
+
when /z$/ then "#{str}zes"
|
146
|
+
when /f$/ then "#{str}ves"
|
147
|
+
when /y$/ then "#{str}ies"
|
148
|
+
when /is$/ then str.sub(/is$/, "es")
|
149
|
+
when "child" then "children"
|
150
|
+
when "person" then "people"
|
151
|
+
when "foot" then "feet"
|
152
|
+
when "photo" then "photos"
|
153
|
+
when /([sxo]|[cs]h)$/ then "#{str}es"
|
154
|
+
else
|
155
|
+
"#{str}s"
|
156
|
+
end
|
123
157
|
end
|
124
158
|
end
|
125
159
|
end
|