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.
@@ -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
@@ -41,7 +41,7 @@ class Kronk
41
41
 
42
42
 
43
43
  ##
44
- # Returns the value from a url, file, or cache as a String.
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 uri == :cache || File.file?(uri)
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
 
@@ -226,7 +226,7 @@ class Kronk
226
226
  data = [parsed_header(options[:with_headers]), data].compact
227
227
  end
228
228
 
229
- DataSet.new(data).modify options
229
+ DataSet.new(data).fetch options
230
230
  end
231
231
  end
232
232
  end
@@ -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
- data_set = Kronk::DataSet.new data
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
- data_set = Kronk::DataSet.new data
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
- data_set = Kronk::DataSet.new data
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
- data_set = Kronk::DataSet.new data
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
@@ -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 nil if no path is found.
12
+ # as an array of keys. Returns false if no path is found.
13
13
 
14
14
  def has_path? path
15
- Kronk::DataSet.new(self).find_data path do |d,k,p|
15
+ Kronk::Path.find path, self do |d,k,p|
16
16
  return !!p
17
17
  end
18
18
 
19
- nil
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
- found = {}
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
- Kronk::DataSet.new(self).find_data path do |d,k,p|
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
- found
105
+ return count > 0
58
106
  end
59
107
  end
60
108
  end
@@ -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 'active_support/inflector'
22
+ require active_support_versions[asupp_i]
23
+
17
24
  rescue LoadError => e
18
- raise unless e.message =~ /-- active_support/
19
- require 'activesupport'
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 && (names.length > 1 ||
121
- parent_name && (names.first == parent_name ||
122
- names.first.pluralize == parent_name))
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