kronk 1.3.1 → 1.4.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 +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
|