sucker 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.md +13 -19
  2. data/lib/sucker.rb +4 -4
  3. data/lib/sucker/parameters.rb +28 -0
  4. data/lib/sucker/request.rb +100 -88
  5. data/lib/sucker/response.rb +10 -0
  6. data/lib/sucker/version.rb +1 -1
  7. data/spec/fixtures/cassette_library/{unit → spec}/sucker/request.yml +4 -4
  8. data/spec/fixtures/cassette_library/spec/sucker/response.yml +26 -0
  9. data/spec/sucker/parameters_spec.rb +61 -0
  10. data/spec/sucker/request_spec.rb +276 -0
  11. data/spec/{unit/sucker → sucker}/response_spec.rb +39 -60
  12. data/spec/{unit/sucker_spec.rb → sucker_spec.rb} +0 -4
  13. data/spec/support/vcr.rb +0 -1
  14. metadata +69 -114
  15. data/spec/fixtures/cassette_library/integration/alternate_versions.yml +0 -26
  16. data/spec/fixtures/cassette_library/integration/errors.yml +0 -26
  17. data/spec/fixtures/cassette_library/integration/france.yml +0 -26
  18. data/spec/fixtures/cassette_library/integration/images.yml +0 -26
  19. data/spec/fixtures/cassette_library/integration/item_lookup/multiple.yml +0 -26
  20. data/spec/fixtures/cassette_library/integration/item_lookup/single.yml +0 -26
  21. data/spec/fixtures/cassette_library/integration/item_search.yml +0 -26
  22. data/spec/fixtures/cassette_library/integration/japan.yml +0 -26
  23. data/spec/fixtures/cassette_library/integration/keyword_search.yml +0 -26
  24. data/spec/fixtures/cassette_library/integration/kindle.yml +0 -26
  25. data/spec/fixtures/cassette_library/integration/kindle_2.yml +0 -26
  26. data/spec/fixtures/cassette_library/integration/multiple_locales.yml +0 -151
  27. data/spec/fixtures/cassette_library/integration/power_search.yml +0 -26
  28. data/spec/fixtures/cassette_library/integration/related_items/child.yml +0 -26
  29. data/spec/fixtures/cassette_library/integration/related_items/parent.yml +0 -26
  30. data/spec/fixtures/cassette_library/integration/seller_listings_search.yml +0 -26
  31. data/spec/fixtures/cassette_library/integration/twenty_items.yml +0 -26
  32. data/spec/fixtures/cassette_library/unit/sucker/response.yml +0 -26
  33. data/spec/integration/alternate_versions_spec.rb +0 -35
  34. data/spec/integration/errors_spec.rb +0 -40
  35. data/spec/integration/france_spec.rb +0 -42
  36. data/spec/integration/images_spec.rb +0 -41
  37. data/spec/integration/item_lookup_spec.rb +0 -71
  38. data/spec/integration/item_search_spec.rb +0 -41
  39. data/spec/integration/japan_spec.rb +0 -35
  40. data/spec/integration/keyword_search_spec.rb +0 -33
  41. data/spec/integration/kindle_spec.rb +0 -55
  42. data/spec/integration/multiple_locales_spec.rb +0 -70
  43. data/spec/integration/power_search_spec.rb +0 -41
  44. data/spec/integration/related_items_spec.rb +0 -53
  45. data/spec/integration/seller_listing_search_spec.rb +0 -32
  46. data/spec/integration/twenty_items_spec.rb +0 -49
  47. data/spec/unit/sucker/request_spec.rb +0 -282
data/README.md CHANGED
@@ -1,21 +1,23 @@
1
1
  Sucker
2
2
  ======
3
3
 
4
- Sucker is a minimal Ruby wrapper to the [Amazon Product Advertising API](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html). It runs on [curb](http://github.com/taf2/curb) and [the Nokogiri implementation of the XML Mini module](http://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini/nokogiri.rb) in Active Support. It's fast and supports __the entire API__.
4
+ Sucker is a [cURL-](http://github.com/taf2/curb) and [Nokogiri-](http://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini/nokogiri.rb)driven Ruby wrapper to the [Amazon Product Advertising API](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html).
5
+
6
+ It's fast and supports __the entire API__.
5
7
 
6
8
  ![Electrolux](https://github.com/papercavalier/sucker/raw/master/electrolux.jpg)
7
9
 
8
10
  Usage
9
11
  -----
10
12
 
11
- Set up a worker.
13
+ Where's your worker?
12
14
 
13
15
  worker = Sucker.new(
14
- :locale => "us",
16
+ :locale => :us,
15
17
  :key => "API KEY",
16
18
  :secret => "API SECRET")
17
19
 
18
- Prepare a request.
20
+ Build a query.
19
21
 
20
22
  worker << {
21
23
  "Operation" => "ItemLookup",
@@ -27,19 +29,15 @@ Get a response.
27
29
 
28
30
  response = worker.get
29
31
 
30
- Make sure nothing went [awry](http://gloss.papercavalier.com/2010/11/01/amazon-call-throttling-demystified.html).
31
-
32
- response.valid?
33
-
34
- Now parse it.
32
+ Parse.
35
33
 
36
- items = response.map("Item") do |item|
37
- # parse item
34
+ books = response.map("Item") do |item|
35
+ # parse
38
36
  end
39
37
 
40
38
  Repeat ad infinitum.
41
39
 
42
- [Check my integration specs](http://github.com/papercavalier/sucker/tree/master/spec/integration/) for more detailed examples. See [twenty items](http://github.com/papercavalier/sucker/tree/master/spec/integration/twenty_items_spec.rb) and [multiple locales](http://github.com/papercavalier/sucker/tree/master/spec/integration/multiple_locales_spec.rb) for relatively advanced usage.
40
+ [Check the features](http://relishapp.com/papercavalier/sucker) for more detailed examples.
43
41
 
44
42
  Read the source code and dive into the [Amazon API docs](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html).
45
43
 
@@ -48,18 +46,14 @@ Stubbing
48
46
 
49
47
  Use [VCR](http://github.com/myronmarston/vcr) to stub your requests.
50
48
 
51
- Match URIs on host only and create a new cassette for each query. [This is how my VCR setup looks like](http://github.com/papercavalier/sucker/blob/master/spec/support/vcr.rb).
49
+ [This is how my VCR setup looks like](http://github.com/papercavalier/sucker/blob/master/spec/support/vcr.rb).
52
50
 
53
51
  Compatibility
54
52
  -------------
55
53
 
56
- Specs pass against Ruby 1.8.7 and Ruby 1.9.2.
57
-
58
- Sucker works seamlessly with or without Rails.
59
-
60
- Sucker won't work on JRuby. (We got Curl under the hood. It's worth the trade-off.)
54
+ Specs pass against Ruby 1.8.7 and Ruby 1.9.2. Sucker has cURL under the hood, so no JRuby.
61
55
 
62
56
  Afterword
63
57
  ---------
64
58
 
65
- No DSL. No object mapping. Pure Nokogiri goodness.
59
+ Don't overabstract a spaghetti API.
data/lib/sucker.rb CHANGED
@@ -1,16 +1,16 @@
1
- require "sucker/request"
2
- require "sucker/response"
1
+ require 'sucker/parameters'
2
+ require 'sucker/request'
3
+ require 'sucker/response'
3
4
 
4
5
  # = Sucker
5
6
  #
6
7
  # Sucker is a Ruby wrapper to the Amazon Product Advertising API.
7
8
  module Sucker
8
- CURRENT_AMAZON_API_VERSION = "2010-10-01"
9
9
 
10
10
  # Initializes a request object
11
11
  #
12
12
  # worker = Sucker.new(
13
- # :locale => "us",
13
+ # :locale => :us,
14
14
  # :key => "API KEY",
15
15
  # :secret => "API SECRET")
16
16
  #
@@ -0,0 +1,28 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Sucker
4
+ class Parameters < Hash
5
+ API_VERSION = '2010-11-01'
6
+ SERVICE = 'AWSECommerceService'
7
+
8
+ def initialize #:nodoc
9
+ self.store 'Service', SERVICE
10
+ self.store 'Version', API_VERSION
11
+ self.store 'Timestamp', timestamp
12
+ end
13
+
14
+ def normalize
15
+ inject({}) do |h, kv|
16
+ k, v = kv
17
+ h[k.to_s.camelize] = v.is_a?(Array) ? v.join(',') : v.to_s
18
+ h
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def timestamp
25
+ Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,7 @@
1
- require "curb"
2
- require "openssl"
3
- require "ostruct"
4
- require "uri"
1
+ require 'curb'
2
+ require 'openssl'
3
+ require 'ostruct'
4
+ require 'uri'
5
5
 
6
6
  module Sucker #:nodoc:
7
7
 
@@ -16,39 +16,25 @@ module Sucker #:nodoc:
16
16
  :jp => 'ecs.amazonaws.jp' }
17
17
  PATH = "/onca/xml"
18
18
 
19
- # The Amazon locale to query
20
- attr_accessor :locale
21
-
22
19
  # The Amazon secret access key
23
20
  attr_accessor :secret
24
21
 
25
- # The hash of parameters to query Amazon with
26
- attr_accessor :parameters
27
-
28
22
  # Initializes a request object
29
23
  #
30
24
  # worker = Sucker.new(
31
- # :locale => "us",
32
25
  # :key => "API KEY",
33
26
  # :secret => "API SECRET")
34
27
  #
35
28
  def initialize(args)
36
- self.parameters = {
37
- "Service" => "AWSECommerceService",
38
- "Version" => CURRENT_AMAZON_API_VERSION
39
- }
40
-
41
29
  args.each { |k, v| send("#{k}=", v) }
42
30
  end
43
31
 
44
- # Merges a hash into existing parameters
32
+ # Merges a hash into the existing parameters
45
33
  #
46
- # worker = Sucker.new
47
34
  # worker << {
48
- # "Operation" => "ItemLookup",
49
- # "IdType" => "ASIN",
50
- # "ItemId" => "0816614024",
51
- # "ResponseGroup" => "ItemAttributes" }
35
+ # "Operation" => "ItemLookup",
36
+ # "IdType" => "ASIN",
37
+ # "ItemId" => "0816614024" }
52
38
  #
53
39
  def <<(hash)
54
40
  self.parameters.merge!(hash)
@@ -56,12 +42,11 @@ module Sucker #:nodoc:
56
42
 
57
43
  # Returns the associate tag for the current locale
58
44
  def associate_tag
59
- associate_tags[locale.to_sym] rescue nil
45
+ @associate_tags[locale.to_sym] rescue ''
60
46
  end
61
47
 
62
48
  # Sets the associate tag for the current locale
63
49
  #
64
- # worker = Sucker.new
65
50
  # worker.associate_tag = 'foo-bar'
66
51
  #
67
52
  def associate_tag=(token)
@@ -80,10 +65,8 @@ module Sucker #:nodoc:
80
65
  # tags = {
81
66
  # :us => 'foo-bar-10',
82
67
  # :uk => 'foo-bar-20',
83
- # :de => 'foo-bar-30',
84
68
  # ... }
85
69
  #
86
- # worker = Sucker.new
87
70
  # worker.associate_tags = tags
88
71
  #
89
72
  def associate_tags=(tokens)
@@ -92,7 +75,6 @@ module Sucker #:nodoc:
92
75
 
93
76
  # Returns options for curl and yields them if given a block
94
77
  #
95
- # worker = Sucker.new
96
78
  # worker.curl_opts { |c| c.interface = "eth1" }
97
79
  #
98
80
  def curl_opts
@@ -104,61 +86,56 @@ module Sucker #:nodoc:
104
86
 
105
87
  # Performs a request and returns a response
106
88
  #
107
- # worker = Sucker.new
108
89
  # response = worker.get
109
90
  #
110
- def get
111
- raise ArgumentError.new "Locale missing" unless locale
112
- raise ArgumentError.new "AWS access key missing" unless key
113
-
114
- curl = Curl::Easy.perform(uri.to_s) do |easy|
115
- curl_opts.each { |k, v| easy.send("#{k}=", v) }
116
- end
117
-
118
- Response.new(curl)
119
- end
120
-
121
- # Performs a request for all locales, returns an array of responses, and yields
122
- # them if given a block
123
- #
124
- # worker = Sucker.new
91
+ # Optionally, pass one or more locales to query specific locales or `:all`
92
+ # to query all locales.
125
93
  #
126
- # # This blocks until all requests are complete
127
- # responses = worker.get_all
94
+ # responses = worker.get(:us)
128
95
  #
129
- # # This does not block
130
- # worker.get_all do |response|
131
- # process_response
132
- # end
96
+ # responses = worker.get(:all)
133
97
  #
134
- def get_all
135
- uris = HOSTS.keys.map do |locale|
136
- self.locale = locale
137
- uri.to_s
138
- end
139
- responses = []
98
+ def get(*args)
99
+ case args.count
140
100
 
141
- Curl::Multi.get(uris, curl_opts) do |curl|
142
- response = Response.new(curl)
143
- yield response if block_given?
144
- responses << response
101
+ when 0
102
+ curl = Curl::Easy.perform(uri.to_s) do |easy|
103
+ curl_opts.each { |k, v| easy.send("#{k}=", v) }
104
+ end
105
+ Response.new(curl)
106
+
107
+ when 1
108
+ arg = args.first
109
+ if arg == :all
110
+ get_multi locales
111
+ else
112
+ self.locale = arg
113
+ get
114
+ end
115
+
116
+ else
117
+ get_multi args
145
118
  end
119
+ end
146
120
 
147
- responses
121
+ def get_all # :nodoc:
122
+ warn "[DEPRECATION] `get_all` is deprecated. Please use `get(:all) instead."
123
+ get(:all)
148
124
  end
149
125
 
150
126
  # Returns the AWS access key for the current locale
151
127
  def key
152
- @keys[locale.to_sym]
128
+ raise ArgumentError.new "AWS access key missing" unless @keys[locale]
129
+
130
+ @keys[locale]
153
131
  end
154
132
 
155
- # Sets a global AWS access key ID
133
+ # Sets a global AWS access key
156
134
  #
157
- # worker = Sucker.new
158
135
  # worker.key = 'foo'
159
136
  #
160
137
  def key=(token)
161
- @keys = HOSTS.keys.inject({}) do |keys, locale|
138
+ @keys = locales.inject({}) do |keys, locale|
162
139
  keys[locale] = token
163
140
  keys
164
141
  end
@@ -170,63 +147,95 @@ module Sucker #:nodoc:
170
147
  # and Canada and (2) the UK, France, and Germany count against the same call
171
148
  # rate quota.
172
149
  #
173
- # # keys = {
150
+ # keys = {
174
151
  # :us => 'foo',
175
152
  # :uk => 'bar',
176
- # :de => 'baz',
177
153
  # ... }
178
154
  #
179
- # worker = Sucker.new
180
155
  # worker.keys = keys
181
156
  #
182
157
  def keys=(tokens)
183
158
  @keys = tokens
184
159
  end
185
160
 
161
+ def locale
162
+ raise ArgumentError.new "Locale not set" unless @locale
163
+
164
+ @locale
165
+ end
166
+
167
+ # Sets the current Amazon locale
168
+ #
169
+ # Valid values are :us, :uk, :de, :ca, :fr, and :jp.
170
+ def locale=(new_locale)
171
+ new_locale = new_locale.to_sym
172
+
173
+ raise ArgumentError.new "Invalid locale" unless locales.include? new_locale
174
+
175
+ @locale = new_locale
176
+ end
177
+
178
+ # The parameters to query Amazon with
179
+ def parameters
180
+ @parameters ||= Parameters.new
181
+ end
182
+
186
183
  # Sets the Amazon API version
187
184
  #
188
- # worker = Sucker.new
189
185
  # worker.version = '2010-06-01'
190
186
  #
191
187
  def version=(version)
192
- self.parameters["Version"] = version
188
+ self.parameters['Version'] = version
193
189
  end
194
190
 
195
191
  private
196
192
 
197
- # Timestamps parameters and concatenates them into a query string
198
193
  def build_query
199
194
  parameters.
200
- merge(timestamp).
201
- merge({ "AWSAccessKeyId" => key }).
202
- merge({ "AssociateTag" => associate_tag }).
195
+ normalize.
196
+ merge({ 'AWSAccessKeyId' => key }).
197
+ merge({ 'AssociateTag' => associate_tag }).
203
198
  sort.
204
- collect do |k, v|
205
- "#{k}=" + escape(v.is_a?(Array) ? v.join(",") : v.to_s)
206
- end.
207
- join("&")
199
+ map do |k, v|
200
+ "#{k}=" + escape(v)
201
+ end.join('&')
208
202
  end
209
203
 
210
- # Returns a signed and timestamped query string
211
204
  def build_signed_query
212
205
  query = build_query
213
206
 
214
- digest = OpenSSL::Digest::Digest.new("sha256")
215
- string = ["GET", host, PATH, query].join("\n")
207
+ digest = OpenSSL::Digest::Digest.new('sha256')
208
+ string = ['GET', host, PATH, query].join("\n")
216
209
  hmac = OpenSSL::HMAC.digest(digest, secret, string)
210
+ signature = escape([hmac].pack('m').chomp)
211
+
212
+ query + '&Signature=' + signature
213
+ end
217
214
 
218
- query + "&Signature=" + escape([hmac].pack("m").chomp)
215
+ def escape(value)
216
+ value.gsub(/([^a-zA-Z0-9_.~-]+)/) do
217
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
218
+ end
219
219
  end
220
220
 
221
- # Plagiarized from the Ruby CGI library via ruby_aaws
222
- def escape(string)
223
- string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
224
- '%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
221
+ def get_multi(locales)
222
+ responses = []
223
+
224
+ Curl::Multi.get(uris, curl_opts) do |curl|
225
+ response = Response.new(curl)
226
+ yield response if block_given?
227
+ responses << response
225
228
  end
229
+
230
+ responses
226
231
  end
227
232
 
228
233
  def host
229
- HOSTS[locale.to_sym]
234
+ HOSTS[locale]
235
+ end
236
+
237
+ def locales
238
+ @locales ||= HOSTS.keys
230
239
  end
231
240
 
232
241
  def uri
@@ -236,8 +245,11 @@ module Sucker #:nodoc:
236
245
  :query => build_signed_query)
237
246
  end
238
247
 
239
- def timestamp
240
- { "Timestamp" => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ') }
248
+ def uris
249
+ locales.map do |locale|
250
+ self.locale = locale
251
+ uri.to_s
252
+ end
241
253
  end
242
254
  end
243
255
  end
@@ -32,6 +32,11 @@ module Sucker #:nodoc:
32
32
  find(path).each { |e| yield e }
33
33
  end
34
34
 
35
+ # Returns an array of errors in the reponse
36
+ def errors
37
+ find("Error")
38
+ end
39
+
35
40
  # Queries an xpath and returns an array of matching nodes
36
41
  #
37
42
  # response = worker.get
@@ -41,6 +46,11 @@ module Sucker #:nodoc:
41
46
  xml.xpath("//xmlns:#{path}").map { |e| strip_content(e.to_hash[path]) }
42
47
  end
43
48
 
49
+ # Returns true if response contains errors
50
+ def has_errors?
51
+ errors.count > 0
52
+ end
53
+
44
54
  # A shorthand that yields matches to a block and collects returned values
45
55
  #
46
56
  # descriptions = worker.get.map("Item") { |item| build_description(item) }