rufus-jig 0.1.23 → 1.0.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.
Files changed (65) hide show
  1. data/.rspec +1 -0
  2. data/CHANGELOG.txt +8 -0
  3. data/README.rdoc +41 -11
  4. data/Rakefile +37 -12
  5. data/TODO.txt +6 -3
  6. data/lib/rufus/jig/adapters/em.rb +21 -24
  7. data/lib/rufus/jig/adapters/net.rb +3 -4
  8. data/lib/rufus/jig/adapters/net_persistent.rb +26 -7
  9. data/lib/rufus/jig/adapters/patron.rb +25 -10
  10. data/lib/rufus/jig/couch.rb +199 -36
  11. data/lib/rufus/jig/http.rb +183 -90
  12. data/lib/rufus/jig/path.rb +4 -4
  13. data/lib/rufus/jig/version.rb +1 -1
  14. data/rufus-jig.gemspec +55 -34
  15. data/spec/couch/attachements_spec.rb +113 -0
  16. data/spec/couch/basic_auth_spec.rb +75 -0
  17. data/spec/couch/conditional_spec.rb +178 -0
  18. data/spec/couch/continuous.rb +97 -0
  19. data/spec/couch/couch_spec.rb +64 -0
  20. data/spec/couch/db_spec.rb +366 -0
  21. data/{test → spec/couch}/tweet.png +0 -0
  22. data/spec/couch/views_spec.rb +326 -0
  23. data/spec/couch_url.txt +2 -0
  24. data/spec/jig/basic_auth_spec.rb +51 -0
  25. data/spec/jig/conditional_spec.rb +76 -0
  26. data/spec/jig/delete_spec.rb +32 -0
  27. data/spec/jig/get_spec.rb +116 -0
  28. data/spec/jig/misc_spec.rb +120 -0
  29. data/spec/jig/new_spec.rb +95 -0
  30. data/spec/jig/parse_uri_spec.rb +139 -0
  31. data/spec/jig/post_spec.rb +79 -0
  32. data/spec/jig/prefix_spec.rb +51 -0
  33. data/spec/jig/put_spec.rb +68 -0
  34. data/spec/jig/timeout_spec.rb +94 -0
  35. data/{test → spec}/server.rb +14 -4
  36. data/spec/spec_helper.rb +61 -0
  37. data/spec/support/couch_helper.rb +14 -0
  38. data/spec/support/server_helper.rb +32 -0
  39. metadata +98 -43
  40. data/lib/rufus/jig/adapters/net_response.rb +0 -42
  41. data/test/base.rb +0 -53
  42. data/test/bm/bm0.rb +0 -49
  43. data/test/bm/bm1.rb +0 -43
  44. data/test/conc/put_vs_delete.rb +0 -28
  45. data/test/couch_base.rb +0 -52
  46. data/test/couch_url.txt +0 -1
  47. data/test/ct_0_couch.rb +0 -64
  48. data/test/ct_1_couchdb.rb +0 -204
  49. data/test/ct_2_couchdb_options.rb +0 -50
  50. data/test/ct_3_couchdb_views.rb +0 -106
  51. data/test/ct_4_attachments.rb +0 -126
  52. data/test/ct_5_couchdb_continuous.rb +0 -92
  53. data/test/cut_0_auth_couch.rb +0 -62
  54. data/test/test.rb +0 -28
  55. data/test/to.sh +0 -25
  56. data/test/tt_0_get_timeout.rb +0 -92
  57. data/test/ut_0_http_get.rb +0 -191
  58. data/test/ut_1_http_post.rb +0 -81
  59. data/test/ut_2_http_delete.rb +0 -42
  60. data/test/ut_3_http_put.rb +0 -105
  61. data/test/ut_4_http_prefix.rb +0 -50
  62. data/test/ut_5_http_misc.rb +0 -65
  63. data/test/ut_6_args.rb +0 -98
  64. data/test/ut_7_parse_uri.rb +0 -79
  65. data/test/ut_8_auth.rb +0 -37
@@ -22,6 +22,8 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
+ require 'cgi'
26
+
25
27
  require 'base64'
26
28
  require 'socket'
27
29
  # for #on_change
@@ -38,7 +40,7 @@ module Rufus::Jig
38
40
  attr_reader :path
39
41
  attr_reader :http
40
42
 
41
- def initialize (*args)
43
+ def initialize(*args)
42
44
 
43
45
  @http = Rufus::Jig::Http.new(*args)
44
46
 
@@ -55,7 +57,7 @@ module Rufus::Jig
55
57
  @http.close
56
58
  end
57
59
 
58
- def put (doc_or_path, opts={})
60
+ def put(doc_or_path, opts={})
59
61
 
60
62
  path, payload = if doc_or_path.is_a?(String)
61
63
  [ doc_or_path, '' ]
@@ -84,19 +86,71 @@ module Rufus::Jig
84
86
  nil
85
87
  end
86
88
 
87
- def get (doc_or_path, opts={})
89
+ def get(doc_or_path, opts={})
88
90
 
89
91
  path = doc_or_path.is_a?(Hash) ? doc_or_path['_id'] : doc_or_path
90
92
  path = adjust(path)
91
93
 
92
- if et = etag(path)
93
- opts[:etag] = et
94
+ @http.get(path, opts)
95
+ end
96
+
97
+ # Returns all the docs in the current database.
98
+ #
99
+ # c = Rufus::Jig::Couch.new('http://127.0.0.1:5984, 'my_db')
100
+ #
101
+ # docs = c.all
102
+ # docs = c.all(:include_docs => false)
103
+ # docs = c.all(:include_design_docs => false)
104
+ #
105
+ # docs = c.all(:skip => 10, :limit => 10)
106
+ #
107
+ # It understands (passes) all the options for CouchDB view API :
108
+ #
109
+ # http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
110
+ #
111
+ def all(opts={})
112
+
113
+ opts = opts.dup
114
+ # don't touch the original
115
+
116
+ path = adjust('_all_docs')
117
+
118
+ opts[:include_docs] = true if opts[:include_docs].nil?
119
+
120
+ adjust_params(opts)
121
+
122
+ keys = opts.delete(:keys)
123
+
124
+ return [] if keys && keys.empty?
125
+
126
+ res = if keys
127
+ opts[:cache] = :with_body if opts[:cache].nil?
128
+ @http.post(path, { 'keys' => keys }, opts)
129
+ else
130
+ @http.get(path, opts)
94
131
  end
95
132
 
96
- @http.get(path, opts)
133
+ rows = res['rows']
134
+
135
+ docs = if opts[:params][:include_docs]
136
+ rows.map { |row| row['doc'] }
137
+ else
138
+ rows.map { |row| { '_id' => row['id'], '_rev' => row['value']['rev'] } }
139
+ end
140
+
141
+ if opts[:include_design_docs] == false
142
+ docs = docs.reject { |doc| DESIGN_PATH_REGEX.match(doc['_id']) }
143
+ end
144
+
145
+ docs
146
+ end
147
+
148
+ def ids(opts={})
149
+
150
+ all(opts).collect { |row| row['_id'] }
97
151
  end
98
152
 
99
- def delete (doc_or_path, rev=nil)
153
+ def delete(doc_or_path, rev=nil)
100
154
 
101
155
  doc, path = if rev
102
156
  [ { '_id' => doc_or_path, '_rev' => rev }, doc_or_path ]
@@ -135,17 +189,9 @@ module Rufus::Jig
135
189
  end
136
190
  end
137
191
 
138
- def post (path, doc)
139
-
140
- path = adjust(path)
141
-
142
- opts = { :content_type => :json }
143
-
144
- if et = etag(path)
145
- opts[:etag] = et
146
- end
192
+ def post(path, doc, opts={})
147
193
 
148
- @http.post(path, doc, opts)
194
+ @http.post(adjust(path), doc, opts.merge(:content_type => :json))
149
195
  end
150
196
 
151
197
  # Attaches a file to a couch document.
@@ -160,7 +206,7 @@ module Rufus::Jig
160
206
  # doc, 'my_picture', data,
161
207
  # :content_type => 'image/jpeg')
162
208
  #
163
- def attach (doc_id, doc_rev, attachment_name, data, opts=nil)
209
+ def attach(doc_id, doc_rev, attachment_name, data, opts=nil)
164
210
 
165
211
  if opts.nil?
166
212
  opts = data
@@ -183,23 +229,27 @@ module Rufus::Jig
183
229
  path = adjust("#{doc_id}/#{attachment_name}?rev=#{doc_rev}")
184
230
 
185
231
  if @http.variant == :patron
186
- #
187
- # patron, as of 0.4.5 has difficulties when PUTting attachements
232
+
233
+ # patron, as of 0.4.5 (~> 0.4.10), has difficulties when PUTting
234
+ # attachements
188
235
  # this is a fallback to net/http
189
- #
236
+
190
237
  require 'net/http'
238
+
191
239
  http = Net::HTTP.new(@http.host, @http.port)
240
+
192
241
  req = Net::HTTP::Put.new(path)
193
242
  req['User-Agent'] =
194
- "rufus-jig #{Rufus::Jig::VERSION} (patron 0.4.5 fallback to net/http)"
243
+ "rufus-jig #{Rufus::Jig::VERSION} (patron 0.4.x fallback to net/http)"
195
244
  req['Content-Type'] =
196
245
  opts[:content_type]
246
+ req['Accept'] =
247
+ 'application/json'
197
248
  req.body = data
198
- res = http.start { |h| h.request(req) }
199
- status = res.code.to_i
200
- raise Rufus::Jig::HttpError.new(status, res.body) \
201
- unless [ 200, 201 ].include?(status)
202
- return nil
249
+
250
+ res = Rufus::Jig::HttpResponse.new(http.start { |h| h.request(req) })
251
+
252
+ return @http.send(:respond, :put, path, nil, opts, nil, res)
203
253
  end
204
254
 
205
255
  @http.put(path, data, opts)
@@ -213,7 +263,7 @@ module Rufus::Jig
213
263
  #
214
264
  # couch.detach(doc, 'my_picture')
215
265
  #
216
- def detach (doc_id, doc_rev, attachment_name=nil)
266
+ def detach(doc_id, doc_rev, attachment_name=nil)
217
267
 
218
268
  if attachment_name.nil?
219
269
  attachment_name = doc_rev
@@ -244,7 +294,7 @@ module Rufus::Jig
244
294
  # Note : doc inclusion (third parameter to the block) only works with
245
295
  # CouchDB >= 0.11.
246
296
  #
247
- def on_change (opts={}, &block)
297
+ def on_change(opts={}, &block)
248
298
 
249
299
  query = {
250
300
  'feed' => 'continuous',
@@ -299,6 +349,8 @@ module Rufus::Jig
299
349
  on_change(opts, &block) if opts[:reconnect]
300
350
  end
301
351
 
352
+ DESIGN_PATH_REGEX = /^\_design\//
353
+
302
354
  # A development method. Removes all the design documents in this couch
303
355
  # database.
304
356
  #
@@ -309,14 +361,111 @@ module Rufus::Jig
309
361
 
310
362
  docs = get('_all_docs')['rows']
311
363
 
312
- views = docs.select { |d| d['id'] && d['id'].match(/^\_design\//) }
364
+ views = docs.select { |d| d['id'] && DESIGN_PATH_REGEX.match(d['id']) }
313
365
 
314
366
  views.each { |v| delete(v['id'], v['value']['rev']) }
315
367
  end
316
368
 
369
+ # Queries a view.
370
+ #
371
+ # res = couch.query('_design/my_test/_view/my_view')
372
+ # #
373
+ # # [ {"id"=>"c3", "key"=>"capuccino", "value"=>nil},
374
+ # # {"id"=>"c0", "key"=>"espresso", "value"=>nil},
375
+ # # {"id"=>"c2", "key"=>"macchiato", "value"=>nil},
376
+ # # {"id"=>"c4", "key"=>"macchiato", "value"=>nil},
377
+ # # {"id"=>"c1", "key"=>"ristretto", "value"=>nil} ]
378
+ #
379
+ # # or simply :
380
+ #
381
+ # res = couch.query('my_test:my_view')
382
+ #
383
+ # Accepts the usual couch parameters : limit, skip, descending, keys,
384
+ # startkey, endkey, ...
385
+ #
386
+ def query(path, opts={})
387
+
388
+ opts = opts.dup
389
+ # don't touch the original
390
+
391
+ raw = opts.delete(:raw)
392
+
393
+ path = if DESIGN_PATH_REGEX.match(path)
394
+ path
395
+ else
396
+ doc_id, view = path.split(':')
397
+ path = "_design/#{doc_id}/_view/#{view}"
398
+ end
399
+
400
+ path = adjust(path)
401
+
402
+ adjust_params(opts)
403
+
404
+ keys = opts.delete(:keys)
405
+
406
+ res = if keys
407
+ opts[:cache] = :with_body if opts[:cache].nil?
408
+ @http.post(path, { 'keys' => keys }, opts)
409
+ else
410
+ @http.get(path, opts)
411
+ end
412
+
413
+ return nil if res == true
414
+ # POST and the view doesn't exist
415
+
416
+ return res if raw
417
+
418
+ res.nil? ? res : res['rows']
419
+ end
420
+
421
+ # A shortcut for
422
+ #
423
+ # query(path, :include_docs => true).collect { |row| row['doc'] }
424
+ #
425
+ def query_for_docs(path, opts={})
426
+
427
+ res = query(path, opts.merge(:include_docs => true))
428
+
429
+ if res.nil?
430
+ nil
431
+ elsif opts[:raw]
432
+ res
433
+ else
434
+ res.collect { |row| row['doc'] }.uniq
435
+ end
436
+ end
437
+
438
+ # Creates or updates docs in bulk (could even delete).
439
+ #
440
+ # http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API#Modify_Multiple_Documents_With_a_Single_Request
441
+ #
442
+ def bulk_put(docs, opts={})
443
+
444
+ res = @http.post(adjust('_bulk_docs'), { 'docs' => docs })
445
+
446
+ opts[:raw] ?
447
+ res :
448
+ res.collect { |row| { '_id' => row['id'], '_rev' => row['rev'] } }
449
+ end
450
+
451
+ # Given an array of documents (at least { '_id' => x, '_rev' => y },
452
+ # deletes them.
453
+ #
454
+ def bulk_delete(docs, opts={})
455
+
456
+ docs = docs.inject([]) { |a, doc|
457
+ a << {
458
+ '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true
459
+ } if doc
460
+ a
461
+ }
462
+
463
+ bulk_put(docs, opts)
464
+ end
465
+
317
466
  protected
318
467
 
319
- def adjust (path)
468
+ def adjust(path)
320
469
 
321
470
  case path
322
471
  when '.' then @path
@@ -325,13 +474,27 @@ module Rufus::Jig
325
474
  end
326
475
  end
327
476
 
328
- # Fetches etag from http cache
329
- #
330
- def etag (path)
477
+ COUCH_PARAMS = %w[
478
+ key startkey endkey descending group group_level limit skip include_docs
479
+ ].collect { |k| k.to_sym }
480
+
481
+ COUCH_KEYS = [ :key, :startkey, :endkey ]
482
+
483
+ def adjust_params(opts)
484
+
485
+ opts[:params] = opts.keys.inject({}) { |h, k|
331
486
 
332
- r = @http.cache[path]
487
+ if COUCH_PARAMS.include?(k)
488
+ v = opts.delete(k)
489
+ if COUCH_KEYS.include?(k)
490
+ h[k] = CGI.escape(Rufus::Json.encode(v))
491
+ elsif v != nil
492
+ h[k] = v
493
+ end
494
+ end
333
495
 
334
- r ? r.first : nil
496
+ h
497
+ }
335
498
  end
336
499
  end
337
500
  end
@@ -22,7 +22,6 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
- require 'ostruct'
26
25
 
27
26
  require 'rufus/lru' # gem install rufus-lru
28
27
 
@@ -34,7 +33,7 @@ module Rufus::Jig
34
33
  # The classical helper method, does a full copy of the given object.
35
34
  # Thanks Marshal.
36
35
  #
37
- def self.marshal_copy (o)
36
+ def self.marshal_copy(o)
38
37
 
39
38
  Marshal.load(Marshal.dump(o))
40
39
  end
@@ -45,7 +44,7 @@ module Rufus::Jig
45
44
 
46
45
  attr_reader :status
47
46
 
48
- def initialize (status, message)
47
+ def initialize(status, message)
49
48
 
50
49
  @status = status
51
50
  super(message)
@@ -56,56 +55,129 @@ module Rufus::Jig
56
55
  #
57
56
  class TimeoutError < RuntimeError
58
57
 
59
- def initialize (message=nil)
58
+ def initialize(message=nil)
60
59
 
61
60
  super(message || 'timed out')
62
61
  end
63
62
  end
64
63
 
64
+ #
65
+ # a Rufus::Jig wrapper for the server response.
66
+ #
65
67
  class HttpResponse
66
68
 
67
69
  attr_reader :status, :headers, :body
68
70
  attr_reader :original
71
+
72
+ def initialize(res)
73
+
74
+ net_http_init(res)
75
+ end
76
+
77
+ def etag
78
+
79
+ headers['ETag'] || headers['Etag'] || headers['etag']
80
+ end
81
+
82
+ protected
83
+
84
+ # (leveraged by the patron adapter as well)
85
+ #
86
+ def net_http_init(net_http_response)
87
+
88
+ @original = net_http_response
89
+ @status = net_http_response.code.to_i
90
+ @body = net_http_response.body
91
+ @headers = {}
92
+ net_http_response.each { |k, v|
93
+ @headers[k.split('-').collect { |s| s.capitalize }.join('-')] = v
94
+ }
95
+ end
69
96
  end
70
97
 
71
- URI_REGEX = /(https?):\/\/([^@]+:[^@]+@)?([^\/]+)([^\?]*)(\?.+)?$/
98
+ #
99
+ # Rufus::Jig.parse_uri returns instances of this class.
100
+ #
101
+ class Uri
102
+
103
+ attr_accessor :scheme
104
+ attr_accessor :username, :password
105
+ attr_accessor :host, :port
106
+ attr_accessor :path, :query, :fragment
107
+
108
+ def initialize(sc, us, ps, ho, po, pa, qu, fr)
109
+
110
+ @scheme = sc
111
+ @username = us
112
+ @password = ps
113
+ @host = ho
114
+ @port = po
115
+ @path = pa
116
+ @query = qu
117
+ @fragment = fr
118
+ end
119
+
120
+ def to_s
121
+
122
+ tail = tail_to_s
123
+
124
+ return tail unless @host
125
+
126
+ up = ''
127
+ up = "#{@username}:#{password}@" if @username
128
+
129
+ "#{@scheme}://#{up}#{@host}:#{@port}#{tail}"
130
+ end
131
+
132
+ def tail_to_s
133
+
134
+ tail = @path
135
+ tail = "#{tail}?#{@query}" if @query
136
+ tail = "#{tail}##{@fragment}" if @fragment
137
+
138
+ tail
139
+ end
140
+ end
141
+
142
+ URI_REGEX = /(https?):\/\/([^@]+:[^@]+@)?([^\/]+)(.*)?$/
143
+ PATH_REGEX = /([^\?#]*)(\?[^#]+)?(#[^#]+)?$/
72
144
 
73
145
  # The current URI lib is not UTF-8 friendly, so this is a workaround.
74
146
  # Temporary hopefully.
75
147
  #
76
- def self.parse_uri (s)
148
+ def self.parse_uri(s)
77
149
 
78
150
  m = URI_REGEX.match(s)
79
151
 
80
- scheme, uname, pass, host, port, path, query = if m
152
+ scheme, uname, pass, host, port, tail = if m
81
153
 
82
154
  ho, po = m[3].split(':')
83
155
  po = (po || 80).to_i
84
156
 
85
- query = m[5] ? m[5][1..-1] : nil
86
-
87
157
  un, pa = m[2] ? m[2][0..-2].split(':') : [ nil, nil ]
88
158
 
89
- [ m[1], un, pa, ho, po, m[4], query ]
159
+ [ m[1], un, pa, ho, po, m[4] ]
90
160
 
91
161
  else
92
162
 
93
- pa, qu = s.split('?')
94
-
95
- [ nil, nil, nil, nil, nil, pa, qu ]
163
+ [ nil, nil, nil, nil, nil, s ]
96
164
  end
97
165
 
98
- OpenStruct.new(
99
- :scheme => scheme,
100
- :host => host, :port => port,
101
- :path => path, :query => query,
102
- :username => uname, :password => pass)
166
+ m = PATH_REGEX.match(tail)
167
+
168
+ path, query, fragment = [ m[1], m[2], m[3] ]
169
+
170
+ port = 443 if scheme == 'https' && port == 80
171
+ query = query[1..-1] if query
172
+ fragment = fragment[1..-1] if fragment
173
+
174
+ Uri.new(scheme, uname, pass, host, port, path, query, fragment)
103
175
  end
104
176
 
105
177
  # The current URI lib is not UTF-8 friendly, so this is a workaround.
106
178
  # Temporary hopefully.
107
179
  #
108
- def self.parse_host (s)
180
+ def self.parse_host(s)
109
181
 
110
182
  u = parse_uri(s)
111
183
 
@@ -140,9 +212,9 @@ module Rufus::Jig
140
212
  # path, it is stored in @_path (and not used).
141
213
  # Rufus::Jig::Couch uses it though.
142
214
  #
143
- attr_accessor :_path, :_query
215
+ attr_accessor :_path, :_query, :_fragment
144
216
 
145
- def initialize (*args)
217
+ def initialize(*args)
146
218
 
147
219
  @options = args.last.is_a?(Hash) ? args.pop.dup : {}
148
220
 
@@ -162,13 +234,19 @@ module Rufus::Jig
162
234
  @options[:basic_auth] ||= [ u.username, u.password ] if u.username
163
235
 
164
236
  if args[1]
165
- @_path, @_query = args[1].split('?')
237
+ uu = Rufus::Jig.parse_uri(args[1])
238
+ @_path = uu.path
239
+ @_query = uu.query
240
+ @_fragment = uu.fragment
166
241
  else
167
242
  @_path = u.path
168
243
  @_query = u.query
244
+ @_fragment = u.fragment
169
245
  end
170
246
  end
171
247
 
248
+ @_path ||= ''
249
+
172
250
  @cache = LruHash.new((@options[:cache_size] || 35).to_i)
173
251
 
174
252
  if pf = @options[:prefix]
@@ -180,99 +258,79 @@ module Rufus::Jig
180
258
 
181
259
  def uri
182
260
 
183
- OpenStruct.new(:scheme => @scheme, :host => @host, :port => @port)
261
+ Uri.new(@scheme, nil, nil, @host, @port, nil, nil, nil)
184
262
  end
185
263
 
186
- def get (path, opts={})
264
+ def get(path, opts={})
187
265
 
188
266
  request(:get, path, nil, opts)
189
267
  end
190
268
 
191
- def post (path, data, opts={})
269
+ def post(path, data, opts={})
192
270
 
193
271
  request(:post, path, data, opts)
194
272
  end
195
273
 
196
- def put (path, data, opts={})
274
+ def put(path, data, opts={})
197
275
 
198
276
  request(:put, path, data, opts)
199
277
  end
200
278
 
201
- def delete (path, opts={})
279
+ def delete(path, opts={})
202
280
 
203
281
  request(:delete, path, nil, opts)
204
282
  end
205
283
 
206
284
  protected
207
285
 
208
- def from_cache (path, opts)
209
-
210
- if et = opts[:etag]
211
-
212
- cached = @cache[path]
213
-
214
- if cached && cached.first != et
215
- #
216
- # cached version is perhaps stale
217
- #
218
- cached = nil
219
- opts.delete(:etag)
220
- end
221
-
222
- cached
223
-
224
- else
225
-
226
- nil
227
- end
228
- end
229
-
230
- def request (method, path, data, opts={})
286
+ def request(method, path, data, opts={})
231
287
 
232
- raw = raw_expected?(opts)
288
+ data_hash = data.hash
233
289
 
234
290
  path = add_prefix(path, opts)
235
291
  path = add_params(path, opts)
236
292
 
237
293
  path = '/' if path == ''
238
294
 
239
- cached = from_cache(path, opts)
240
- opts.delete(:etag) if (not cached) || method != :get
295
+ cached = from_cache(method, path, data_hash, opts)
241
296
 
242
297
  opts = rehash_options(opts)
243
298
  data = repack_data(data, opts)
244
299
 
245
- r = do_request(method, path, data, opts)
300
+ res = do_request(method, path, data, opts)
246
301
 
247
- @last_response = r
302
+ respond(method, path, data_hash, opts, cached, res)
303
+ end
248
304
 
249
- unless raw
305
+ def respond(method, path, data_hash, opts, cached, res)
250
306
 
251
- return Rufus::Jig.marshal_copy(cached.last) if r.status == 304
252
- return nil if method == :get && r.status == 404
253
- return true if [ 404, 409 ].include?(r.status)
307
+ @last_response = res
254
308
 
255
- raise @error_class.new(r.status, r.body) \
256
- if r.status >= 400 && r.status < 600
257
- end
309
+ raw = opts[:raw]
310
+ raw == false ? false : raw || @options[:raw]
258
311
 
259
- b = decode_body(r, opts)
312
+ unless raw
260
313
 
261
- do_cache(method, path, r, b, opts)
314
+ return Rufus::Jig.marshal_copy(cached.last) if res.status == 304
315
+ return nil if method == :get && res.status == 404
316
+ return true if [ 404, 409 ].include?(res.status)
262
317
 
263
- raw ? r : b
264
- end
318
+ if res.status >= 400 && res.status < 600
319
+ #File.open('error.html', 'wb') { |f| f.puts(res.body) }
320
+ raise @error_class.new(res.status, res.body)
321
+ end
322
+ end
265
323
 
266
- def raw_expected? (opts)
324
+ b = decode_body(res, opts)
267
325
 
268
- raw = opts[:raw]
326
+ do_cache(method, path, data_hash, opts, res, b)
269
327
 
270
- raw == false ? false : raw || @options[:raw]
328
+ raw ? res : b
271
329
  end
272
330
 
273
331
  # Should work with GET and POST/PUT options
274
332
  #
275
- def rehash_options (opts)
333
+ def rehash_options(opts)
276
334
 
277
335
  opts['Accept'] ||= (opts.delete(:accept) || 'application/json')
278
336
  opts['Accept'] = 'application/json' if opts['Accept'] == :json
@@ -291,7 +349,7 @@ module Rufus::Jig
291
349
  opts
292
350
  end
293
351
 
294
- def add_prefix (path, opts)
352
+ def add_prefix(path, opts)
295
353
 
296
354
  host = Rufus::Jig.parse_host(path)
297
355
 
@@ -306,7 +364,7 @@ module Rufus::Jig
306
364
  Path.join(*elts)
307
365
  end
308
366
 
309
- def add_params (path, opts)
367
+ def add_params(path, opts)
310
368
 
311
369
  if params = opts[:params]
312
370
 
@@ -322,42 +380,77 @@ module Rufus::Jig
322
380
  path
323
381
  end
324
382
 
325
- def repack_data (data, opts)
383
+ APP_JSON_REGEX = /^application\/json/
326
384
 
327
- return nil unless data
385
+ def repack_data(data, opts)
328
386
 
387
+ return nil if data == nil
388
+ return data if data.is_a?(String)
329
389
 
330
- data = if data.is_a?(String)
331
- data
332
- elsif (opts['Content-Type'] || '').match(/^application\/json/)
333
- Rufus::Json.encode(data)
390
+ ct = opts['Content-Type'] || ''
391
+
392
+ if APP_JSON_REGEX.match(ct)
393
+ return Rufus::Json.encode(data)
394
+ end
395
+ if ct == '' && (data.is_a?(Array) || data.is_a?(Hash))
396
+ opts['Content-Type'] = 'application/json'
397
+ return Rufus::Json.encode(data)
398
+ end
399
+
400
+ data.to_s
401
+ end
402
+
403
+ def from_cache(method, path, data_hash, opts)
404
+
405
+ path = if method == :post && opts[:cache] == :with_body
406
+ "#{path}//#{data_hash}"
334
407
  else
335
- data.to_s
408
+ path
336
409
  end
337
410
 
338
- #opts['Content-Length'] =
339
- # (data.respond_to?(:bytesize) ? data.bytesize : data.size).to_s
340
- #
341
- # Patron doesn't play well with custom lengths : "56, 56" issue
411
+ etag = opts[:etag]
412
+ cached = @cache[path]
413
+
414
+ if etag && cached && cached.first != etag
415
+ # cached version is probably stale
416
+ cached = nil
417
+ opts.delete(:etag)
418
+ end
419
+ if ( ! etag) && cached
420
+ opts[:etag] = cached.first
421
+ end
342
422
 
343
- data
423
+ cached
344
424
  end
345
425
 
346
- def do_cache (method, path, response, body, opts)
426
+ def do_cache(method, path, data_hash, opts, response, body)
427
+
428
+ path = if method == :post && opts[:cache] == :with_body
429
+ "#{path}//#{data_hash}"
430
+ else
431
+ path
432
+ end
347
433
 
348
- if (method != :get) || (opts[:cache] == false)
434
+ etag = response.etag
435
+
436
+ if etag.nil? || opts[:cache] == false || method == :delete
437
+ @cache.delete(path)
438
+ return
439
+ end
440
+ if method != :get && ( ! opts[:cache])
349
441
  @cache.delete(path)
350
- elsif et = response.headers['Etag']
351
- @cache[path] = [ et, Rufus::Jig.marshal_copy(body) ]
442
+ return
352
443
  end
444
+
445
+ @cache[path] = [ etag, Rufus::Jig.marshal_copy(body) ]
353
446
  end
354
447
 
355
- def decode_body (response, opts)
448
+ def decode_body(response, opts)
356
449
 
357
450
  b = response.body
358
451
  ct = response.headers['Content-Type']
359
452
 
360
- if opts[:force_json] || (ct && ct.match(/^application\/json/))
453
+ if opts[:force_json] || (ct && APP_JSON_REGEX.match(ct))
361
454
  Rufus::Json.decode(b)
362
455
  else
363
456
  b