kronk 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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