sucker 1.1.4 → 1.2.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 (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) }