amazon_product 3.0.0.pre.1
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.
- data/LICENSE +13 -0
- data/README.md +45 -0
- data/lib/amazon_product/error.rb +13 -0
- data/lib/amazon_product/hash_builder.rb +47 -0
- data/lib/amazon_product/locale.rb +33 -0
- data/lib/amazon_product/operations.rb +110 -0
- data/lib/amazon_product/request.rb +110 -0
- data/lib/amazon_product/response.rb +71 -0
- data/lib/amazon_product/synchrony.rb +34 -0
- data/lib/amazon_product/version.rb +3 -0
- data/lib/amazon_product.rb +26 -0
- data/spec/amazon_product/hash_builder_spec.rb +38 -0
- data/spec/amazon_product/request_spec.rb +189 -0
- data/spec/amazon_product/response_spec.rb +95 -0
- data/spec/amazon_product/synchrony_spec.rb +49 -0
- data/spec/fixtures/http_response +1 -0
- data/spec/spec_helper.rb +9 -0
- metadata +82 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Amazon Product
|
2
|
+
|
3
|
+
Amazon Product is a Ruby wrapper to the [Amazon Product Advertising API] [1].
|
4
|
+
|
5
|
+
[](http://travis-ci.org/hakanensari/amazon_product)
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Require.
|
10
|
+
|
11
|
+
Set up a request.
|
12
|
+
|
13
|
+
request = AmazonProduct["us"]
|
14
|
+
|
15
|
+
request.configure do |c|
|
16
|
+
c.key = YOUR_AMAZON_KEY
|
17
|
+
c.secret = YOUR_AMAZON_SECRET
|
18
|
+
c.tag = YOUR_AMAZON_ASSOCIATE_TAG
|
19
|
+
end
|
20
|
+
|
21
|
+
Look up a product.
|
22
|
+
|
23
|
+
request << { :operation' => 'ItemLookup',
|
24
|
+
:item_id' => '0679753354' }
|
25
|
+
response = request.get
|
26
|
+
|
27
|
+
[Or use a shorthand] [2].
|
28
|
+
|
29
|
+
response = req.find('0679753354')
|
30
|
+
|
31
|
+
Consume the entire response.
|
32
|
+
|
33
|
+
response.to_hash
|
34
|
+
|
35
|
+
Quickly drop down to a particular node.
|
36
|
+
|
37
|
+
response['Item']
|
38
|
+
|
39
|
+
Please see [the project page] [3] for more detailed info.
|
40
|
+
|
41
|
+
[1]: https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html
|
42
|
+
|
43
|
+
[2]: https://github.com/hakanensari/amazon_product/blob/master/lib/amazon_product/operations.rb
|
44
|
+
|
45
|
+
[3]: http://code.papercavalier.com/amazon_product/
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
# Raised when a bad locale is specified.
|
3
|
+
class BadLocale < ArgumentError; end
|
4
|
+
|
5
|
+
# Raised when the Amazon key is not specified.
|
6
|
+
class MissingKey < ArgumentError; end
|
7
|
+
|
8
|
+
# Raised when the Amazon secret is not specified.
|
9
|
+
class MissingSecret < ArgumentError; end
|
10
|
+
|
11
|
+
# Raised when the Amazon associate tag is not specified.
|
12
|
+
class MissingTag < ArgumentError; end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
module HashBuilder
|
3
|
+
# Builds a hash from a Nokogiri XML document.
|
4
|
+
#
|
5
|
+
# In earlier versions of Sucker, I was relying on the XML Mini
|
6
|
+
# Nokogiri module in Active Support. This method essentially
|
7
|
+
# accomplishes the same.
|
8
|
+
#
|
9
|
+
# Based on https://gist.github.com/335286
|
10
|
+
def self.from_xml(xml)
|
11
|
+
case xml
|
12
|
+
when Nokogiri::XML::Document
|
13
|
+
from_xml(xml.root)
|
14
|
+
when Nokogiri::XML::Element
|
15
|
+
result_hash = {}
|
16
|
+
|
17
|
+
xml.attributes.each_pair do |key, attribute|
|
18
|
+
result_hash[key] = attribute.value
|
19
|
+
end
|
20
|
+
|
21
|
+
xml.children.each do |child|
|
22
|
+
result = from_xml(child)
|
23
|
+
|
24
|
+
if child.name == 'text'
|
25
|
+
if result_hash.empty?
|
26
|
+
return result
|
27
|
+
else
|
28
|
+
result_hash['__content__'] = result
|
29
|
+
end
|
30
|
+
elsif result_hash[child.name]
|
31
|
+
if result_hash[child.name].is_a? Array
|
32
|
+
result_hash[child.name] << result
|
33
|
+
else
|
34
|
+
result_hash[child.name] = [result_hash[child.name]] << result
|
35
|
+
end
|
36
|
+
else
|
37
|
+
result_hash[child.name] = result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
result_hash
|
42
|
+
else
|
43
|
+
xml.content.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
# An Amazon locale.
|
3
|
+
class Locale
|
4
|
+
# Available Amazon hosts.
|
5
|
+
HOSTS = { :ca => 'ecs.amazonaws.ca',
|
6
|
+
:cn => 'webservices.amazon.cn',
|
7
|
+
:de => 'ecs.amazonaws.de',
|
8
|
+
:fr => 'ecs.amazonaws.fr',
|
9
|
+
:it => 'webservices.amazon.it',
|
10
|
+
:jp => 'ecs.amazonaws.jp',
|
11
|
+
:us => 'ecs.amazonaws.com',
|
12
|
+
:uk => 'ecs.amazonaws.co.uk' }
|
13
|
+
|
14
|
+
# The Amazon Web Services access key.
|
15
|
+
attr_accessor :key
|
16
|
+
|
17
|
+
# The Amazon Web Services secret.
|
18
|
+
attr_accessor :secret
|
19
|
+
|
20
|
+
# The Amazon associate tag.
|
21
|
+
attr_accessor :tag
|
22
|
+
|
23
|
+
def initialize(locale)
|
24
|
+
raise BadLocale unless HOSTS.has_key?(locale)
|
25
|
+
@locale = locale
|
26
|
+
end
|
27
|
+
|
28
|
+
# The Amazon host.
|
29
|
+
def host
|
30
|
+
HOSTS[@locale]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
# Some shorthand notation for available operations.
|
3
|
+
module Operations
|
4
|
+
# Cart operations.
|
5
|
+
def add_to_cart(params)
|
6
|
+
cart 'Add', params
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear_cart(params)
|
10
|
+
cart 'Clear', params
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_cart(params)
|
14
|
+
cart 'Create', params
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_cart(params)
|
18
|
+
cart 'Get', params
|
19
|
+
end
|
20
|
+
|
21
|
+
def modify_cart(params)
|
22
|
+
cart 'Modify', params
|
23
|
+
end
|
24
|
+
|
25
|
+
# Given up to ten item ids, returns some or all of the item attributes,
|
26
|
+
# depending on the response group specified in the request.
|
27
|
+
#
|
28
|
+
# Id Type defaults to ASIN.
|
29
|
+
#
|
30
|
+
# Assuming you have a request object, the following returns some basic
|
31
|
+
# information for the ASIN 0679753354:
|
32
|
+
#
|
33
|
+
# request.find('0679753354')
|
34
|
+
#
|
35
|
+
# The following request returns cover art for the same ASIN:
|
36
|
+
#
|
37
|
+
# req.find('0679753354', :response_group => 'Images')
|
38
|
+
#
|
39
|
+
def find(*item_ids)
|
40
|
+
reset
|
41
|
+
params = item_ids.last.is_a?(Hash) ? item_ids.pop : {}
|
42
|
+
self.<< ({ 'Operation' => 'ItemLookup',
|
43
|
+
'ItemId' => item_ids }).merge(params)
|
44
|
+
get
|
45
|
+
end
|
46
|
+
|
47
|
+
# Given a browse node ID, returns the specified browse node’s name,
|
48
|
+
# children, and ancestors.
|
49
|
+
def find_browse_node(browse_node_id, params = {})
|
50
|
+
reset
|
51
|
+
self.<< ({ 'Operation' => 'BrowseNodeLookup',
|
52
|
+
'BrowseNodeId' => browse_node_id }).merge(params)
|
53
|
+
get
|
54
|
+
end
|
55
|
+
|
56
|
+
# Given up to ten item ids, returns up to ten products per page that are
|
57
|
+
# similar to those items.
|
58
|
+
def find_similar(*item_ids)
|
59
|
+
reset
|
60
|
+
params = item_ids.last.is_a?(Hash) ? item_ids.pop : {}
|
61
|
+
self.<< ({ 'Operation' => 'SimilarityLookup',
|
62
|
+
'ItemId' => item_ids }).merge(params)
|
63
|
+
get
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns up to ten items that satisfy the search criteria, including one
|
67
|
+
# or more search indices.
|
68
|
+
#
|
69
|
+
# Assuming you have a request object, the following searches the entire
|
70
|
+
# Amazon catalog for the keyword 'book':
|
71
|
+
#
|
72
|
+
# request.search('book')
|
73
|
+
#
|
74
|
+
# The following searches the books search index for the keyword 'lacan':
|
75
|
+
#
|
76
|
+
# request.search('Books', 'lacan')
|
77
|
+
#
|
78
|
+
# The following runs a power search on the books search index for non-
|
79
|
+
# fiction titles authored by Lacan and sorts results by Amazon's relevance
|
80
|
+
# ranking:
|
81
|
+
#
|
82
|
+
# request.search('Books', :power => 'author:lacan and not fiction',
|
83
|
+
# :sort => 'relevancerank')
|
84
|
+
#
|
85
|
+
def search(search_index = nil, params = nil)
|
86
|
+
reset
|
87
|
+
|
88
|
+
if params.nil?
|
89
|
+
params = { 'Keywords' => search_index }
|
90
|
+
search_index = 'All'
|
91
|
+
end
|
92
|
+
|
93
|
+
if params.is_a? String
|
94
|
+
params = { 'Keywords' => params }
|
95
|
+
end
|
96
|
+
|
97
|
+
self.<< ({ 'Operation' => 'ItemSearch',
|
98
|
+
'SearchIndex' => search_index }.merge(params))
|
99
|
+
get
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def cart(operation, params)
|
105
|
+
reset
|
106
|
+
self.<< ({ 'Operation' => "Cart#{operation}" }.merge(params))
|
107
|
+
get
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
# A wrapper around the API request.
|
3
|
+
class Request
|
4
|
+
extend Forwardable
|
5
|
+
include Operations
|
6
|
+
|
7
|
+
# The latest Amazon API version. See:
|
8
|
+
# http://aws.amazon.com/archives/Product%20Advertising%20API
|
9
|
+
CURRENT_API_VERSION = '2011-08-01'
|
10
|
+
|
11
|
+
# The Amazon locale.
|
12
|
+
attr :locale
|
13
|
+
|
14
|
+
def_delegators :locale, :host, :key, :secret, :tag
|
15
|
+
|
16
|
+
# Creates a new request for specified locale.
|
17
|
+
def initialize(locale)
|
18
|
+
@locale = Locale.new(locale.to_sym)
|
19
|
+
@params = Hash.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Merges a hash of request parameters into the query.
|
23
|
+
#
|
24
|
+
# request << { :key => 'value }
|
25
|
+
#
|
26
|
+
def <<(hash)
|
27
|
+
hash.each do |k, v|
|
28
|
+
# Cast value to string.
|
29
|
+
v = v.is_a?(Array) ? v.join(',') : v.to_s
|
30
|
+
|
31
|
+
# Camelize key.
|
32
|
+
k = k.to_s.split('_').map { |w| w[0, 1] = w[0, 1].upcase; w }.join
|
33
|
+
|
34
|
+
@params[k] = v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Configures the Amazon locale.
|
39
|
+
#
|
40
|
+
# request.configure do |c|
|
41
|
+
# c.key = YOUR_KEY
|
42
|
+
# c.secret = YOUR_SECRET
|
43
|
+
# c.tag = YOUR_ASSOCIATE_TAG
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
def configure
|
47
|
+
yield locale
|
48
|
+
end
|
49
|
+
|
50
|
+
# The request parameters.
|
51
|
+
def params
|
52
|
+
raise MissingKey unless key
|
53
|
+
raise MissingTag unless tag
|
54
|
+
|
55
|
+
{ 'AWSAccessKeyId' => key,
|
56
|
+
'AssociateTag' => tag,
|
57
|
+
'Service' => 'AWSECommerceService',
|
58
|
+
'Timestamp' => timestamp,
|
59
|
+
'Version' => CURRENT_API_VERSION }.merge(@params)
|
60
|
+
end
|
61
|
+
|
62
|
+
# A string representation of the request parameters.
|
63
|
+
def query
|
64
|
+
params.sort.map { |k, v| "#{k}=" + escape(v) }.join('&')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Resets the request parameters.
|
68
|
+
def reset
|
69
|
+
@params = Hash.new
|
70
|
+
end
|
71
|
+
|
72
|
+
# Performs a request.
|
73
|
+
def get
|
74
|
+
resp = Net::HTTP.get_response(url)
|
75
|
+
Response.new(resp)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds a signature to a query
|
79
|
+
def sign(unsigned_query)
|
80
|
+
raise MissingSecret unless secret
|
81
|
+
|
82
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
83
|
+
url_string = ['GET', host, '/onca/xml', unsigned_query].join("\n")
|
84
|
+
hmac = OpenSSL::HMAC.digest(digest, secret, url_string)
|
85
|
+
signature = escape([hmac].pack('m').chomp)
|
86
|
+
|
87
|
+
"#{unsigned_query}&Signature=#{signature}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# The current timestamp.
|
91
|
+
def timestamp
|
92
|
+
Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
93
|
+
end
|
94
|
+
|
95
|
+
# The Amazon URL.
|
96
|
+
def url
|
97
|
+
URI::HTTP.build(:host => host,
|
98
|
+
:path => '/onca/xml',
|
99
|
+
:query => sign(query))
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def escape(value)
|
105
|
+
value.gsub(/([^a-zA-Z0-9_.~-]+)/) do
|
106
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module AmazonProduct
|
2
|
+
# A wrapper around the API response.
|
3
|
+
class Response
|
4
|
+
|
5
|
+
# The response body.
|
6
|
+
attr_accessor :body
|
7
|
+
|
8
|
+
# The HTTP status code of the response.
|
9
|
+
attr_accessor :code
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
self.body = response.body
|
13
|
+
self.code = response.code.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
# A shorthand that queries for a specified attribute and yields to a given
|
17
|
+
# block each matching document.
|
18
|
+
#
|
19
|
+
# response.each('Item') { |item| puts item }
|
20
|
+
#
|
21
|
+
def each(path, &block)
|
22
|
+
find(path).each { |match| block.call(match) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# An array of errors in the response.
|
26
|
+
def errors
|
27
|
+
find('Error')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Queries for a specified attribute and returns an array of matching
|
31
|
+
# documents.
|
32
|
+
#
|
33
|
+
# items = response.find('Item')
|
34
|
+
#
|
35
|
+
def find(attribute)
|
36
|
+
xml.xpath("//xmlns:#{attribute}").map do |element|
|
37
|
+
HashBuilder.from_xml(element)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias [] find
|
41
|
+
|
42
|
+
# Returns true if the response contains errors.
|
43
|
+
def has_errors?
|
44
|
+
errors.count > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# A shorthand that queries for a specifed attribute, yields to a given
|
48
|
+
# block matching documents, and collects final values.
|
49
|
+
#
|
50
|
+
# items = response.map('Item') { |item| # do something }
|
51
|
+
#
|
52
|
+
def map(path, &block)
|
53
|
+
find(path).map { |match| block.call(match) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parses the response into a simple hash.
|
57
|
+
def to_hash
|
58
|
+
HashBuilder.from_xml(xml)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Checks if the HTTP response is OK.
|
62
|
+
def valid?
|
63
|
+
code == 200
|
64
|
+
end
|
65
|
+
|
66
|
+
# The XML document.
|
67
|
+
def xml
|
68
|
+
@xml ||= Nokogiri::XML(body)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'amazon_product'
|
2
|
+
require 'em-synchrony'
|
3
|
+
require 'em-synchrony/em-http'
|
4
|
+
|
5
|
+
# Patches Request and Response to make them fiber-aware.
|
6
|
+
module AmazonProduct
|
7
|
+
class Request
|
8
|
+
def adapter
|
9
|
+
@adapter ||= EM::HttpRequest
|
10
|
+
end
|
11
|
+
|
12
|
+
# Performs an evented request.
|
13
|
+
#
|
14
|
+
# Yields a response to given block.
|
15
|
+
def aget(&block)
|
16
|
+
http = EM::HttpRequest.new(url).aget
|
17
|
+
http.callback { block.call(Response.new(http)) }
|
18
|
+
http.errback { block.call(Response.new(http)) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Performs an evented request.
|
22
|
+
def get
|
23
|
+
http = EM::HttpRequest.new(url).get
|
24
|
+
Response.new(http)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Response
|
29
|
+
def initialize(http)
|
30
|
+
self.body = http.response
|
31
|
+
self.code = http.response_header.status
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'net/http'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
require 'amazon_product/error'
|
7
|
+
require 'amazon_product/hash_builder'
|
8
|
+
require 'amazon_product/locale'
|
9
|
+
require 'amazon_product/operations'
|
10
|
+
require 'amazon_product/request'
|
11
|
+
require 'amazon_product/response'
|
12
|
+
|
13
|
+
# Amazon Product is a Ruby wrapper to the Amazon Product Advertising API.
|
14
|
+
module AmazonProduct
|
15
|
+
@requests = Hash.new
|
16
|
+
|
17
|
+
# A request.
|
18
|
+
#
|
19
|
+
# Takes an Amazon locale as argument. This can be +ca+, +cn+, +de+, +fr+,
|
20
|
+
# +it+, +jp+, +uk+, or +us+.
|
21
|
+
#
|
22
|
+
# The library will cache one request per locale.
|
23
|
+
def self.[](locale)
|
24
|
+
@requests[locale] ||= Request.new(locale)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AmazonProduct
|
4
|
+
describe HashBuilder do
|
5
|
+
let(:xml) do
|
6
|
+
xml = <<-XML.gsub!(/>\s+</, '><').strip!
|
7
|
+
<?xml version=\"1.0\" ?>
|
8
|
+
<ItemAttributes>
|
9
|
+
<Title>Anti-Oedipus</Title>
|
10
|
+
<Author>Gilles Deleuze</Author>
|
11
|
+
<Author>Felix Guattari</Author>
|
12
|
+
<Creator Role="Translator">Robert Hurley</Creator>
|
13
|
+
</ItemAttributes>
|
14
|
+
XML
|
15
|
+
Nokogiri::XML(xml)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.from_xml' do
|
19
|
+
it 'returns a hash' do
|
20
|
+
HashBuilder.from_xml(xml).should be_an_instance_of Hash
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'handles only childs' do
|
24
|
+
HashBuilder.from_xml(xml)['Title'].should eql 'Anti-Oedipus'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'handles arrays' do
|
28
|
+
HashBuilder.from_xml(xml)['Author'].should be_a Array
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'handles attributes' do
|
32
|
+
node = HashBuilder.from_xml(xml)['Creator']
|
33
|
+
node['Role'].should eql 'Translator'
|
34
|
+
node['__content__'].should eql 'Robert Hurley'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AmazonProduct
|
4
|
+
describe Request do
|
5
|
+
|
6
|
+
subject { Request.new('us') }
|
7
|
+
|
8
|
+
describe '#<<' do
|
9
|
+
before do
|
10
|
+
subject.configure do |c|
|
11
|
+
c.key = 'foo'
|
12
|
+
c.tag = 'bar'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'merges parameters into the query' do
|
17
|
+
subject << { 'Key' => 'value' }
|
18
|
+
subject.params['Key'].should eql 'value'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'camelizes keys' do
|
22
|
+
subject << { :some_key => 'value' }
|
23
|
+
subject.params.should have_key 'SomeKey'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'does not modify already-camelized keys' do
|
27
|
+
subject << { 'SomeKey' => 'value' }
|
28
|
+
subject.params.should have_key 'SomeKey'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'casts numeric values to string' do
|
32
|
+
subject << { 'Key' => 1 }
|
33
|
+
subject.params['Key'].should eql '1'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'converts array values to string' do
|
37
|
+
subject << { 'Key' => ['foo', 'bar'] }
|
38
|
+
subject.params['Key'].should eql 'foo,bar'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#configure' do
|
43
|
+
it 'configures the locale' do
|
44
|
+
subject.configure do |c|
|
45
|
+
c.key = 'foo'
|
46
|
+
end
|
47
|
+
|
48
|
+
locale = subject.locale
|
49
|
+
locale.key.should eql 'foo'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#get' do
|
54
|
+
before do
|
55
|
+
subject.configure do |c|
|
56
|
+
c.key = 'foo'
|
57
|
+
c.secret = 'bar'
|
58
|
+
c.tag = 'baz'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns a response' do
|
63
|
+
response = subject.get
|
64
|
+
response.should be_a Response
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#params' do
|
69
|
+
context 'when no credentials are specified' do
|
70
|
+
it 'raises an error' do
|
71
|
+
expect do
|
72
|
+
subject.params
|
73
|
+
end.to raise_error MissingKey
|
74
|
+
|
75
|
+
expect do
|
76
|
+
subject.locale.key = 'foo'
|
77
|
+
subject.params
|
78
|
+
end.to raise_error MissingTag
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when credentials are specified' do
|
83
|
+
before do
|
84
|
+
subject.configure do |c|
|
85
|
+
c.key = 'foo'
|
86
|
+
c.tag = 'bar'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'returns the request parameters' do
|
91
|
+
subject.params['Service'].should eql 'AWSECommerceService'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'includes credentials' do
|
95
|
+
subject.params.should have_key 'AWSAccessKeyId'
|
96
|
+
subject.params.should have_key 'AssociateTag'
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'includes a timestamp' do
|
100
|
+
subject.params.should have_key 'Timestamp'
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when no API version is specified' do
|
104
|
+
it 'includes the current API version' do
|
105
|
+
subject.params['Version'].should eql Request::CURRENT_API_VERSION
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'when an API version is specified' do
|
110
|
+
it 'includes that API version' do
|
111
|
+
subject << { 'Version' => 'foo' }
|
112
|
+
subject.params['Version'].should eql 'foo'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#query' do
|
119
|
+
before do
|
120
|
+
subject.configure do |c|
|
121
|
+
c.key = 'foo'
|
122
|
+
c.tag = 'bar'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'canonicalizes the request parameters' do
|
127
|
+
subject.query.should match /\w+=\w+&/
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'sorts the request parameters' do
|
131
|
+
subject << { 'A' => 1 }
|
132
|
+
subject.query.should match /^A=1&/
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'URL-encodes values' do
|
136
|
+
subject << { :key => 'foo,bar' }
|
137
|
+
subject.query.should match /foo%2Cbar/
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#reset' do
|
142
|
+
before do
|
143
|
+
subject.configure do |c|
|
144
|
+
c.key = 'foo'
|
145
|
+
c.tag = 'bar'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'resets the request parameters' do
|
150
|
+
subject << { 'Key' => 'value' }
|
151
|
+
subject.params.should have_key 'Key'
|
152
|
+
|
153
|
+
subject.reset
|
154
|
+
subject.params.should_not have_key 'Key'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#sign' do
|
159
|
+
it 'adds a signature to a query' do
|
160
|
+
subject.locale.secret = 'baz'
|
161
|
+
subject.sign('foo').should match /^foo&Signature=/
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'raises an error if no secret is specified' do
|
165
|
+
expect { subject.sign('foo') }.to raise_error MissingSecret
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe '#timestamp' do
|
170
|
+
it 'generates a timestamp' do
|
171
|
+
subject.timestamp.should match /^\d+-\d+-\d+T\d+:\d+:\d+Z$/
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '#url' do
|
176
|
+
before do
|
177
|
+
subject.configure do |c|
|
178
|
+
c.key = 'foo'
|
179
|
+
c.secret = 'bar'
|
180
|
+
c.tag = 'baz'
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'builds a URL' do
|
185
|
+
subject.url.should be_a URI::HTTP
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AmazonProduct
|
4
|
+
describe Response do
|
5
|
+
let(:response) do
|
6
|
+
http_resp = Struct.new(:body, :code).new
|
7
|
+
http_resp.body = File.read(File.expand_path('../../fixtures/http_response', __FILE__))
|
8
|
+
http_resp.code = '200'
|
9
|
+
Response.new(http_resp)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#each' do
|
13
|
+
context 'when a block is given' do
|
14
|
+
it 'yields matches to a block' do
|
15
|
+
yielded = false
|
16
|
+
response.each('Item') do |item|
|
17
|
+
yielded = true
|
18
|
+
end
|
19
|
+
|
20
|
+
yielded.should be_true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#errors' do
|
26
|
+
it 'returns an array of errors' do
|
27
|
+
response.body = <<-XML.gsub!(/>\s+</, '><').strip!
|
28
|
+
<?xml version=\"1.0\" ?>
|
29
|
+
<Response xmlns="http://example.com">
|
30
|
+
<Errors>
|
31
|
+
<Error>foo</Error>
|
32
|
+
</Errors>
|
33
|
+
</Response>
|
34
|
+
XML
|
35
|
+
response.errors.should =~ ['foo']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#has_errors?' do
|
40
|
+
context 'when a response does not contain any errors' do
|
41
|
+
it 'returns false' do
|
42
|
+
response.stub!(:errors).and_return([])
|
43
|
+
response.should_not have_errors
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when a response contains errors' do
|
48
|
+
it 'returns true' do
|
49
|
+
response.stub!(:errors).and_return([1])
|
50
|
+
response.should have_errors
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#find' do
|
56
|
+
it 'returns an array of matching nodes' do
|
57
|
+
response.find('ASIN').should_not be_empty
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#map" do
|
62
|
+
it "yields each match to a block and maps returned values" do
|
63
|
+
titles = response.map('Item') { |item| item['ItemAttributes']['Title'] }
|
64
|
+
titles.count.should eql 2
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#to_hash' do
|
69
|
+
it 'casts response to a hash' do
|
70
|
+
response.to_hash.should be_a Hash
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#valid?' do
|
75
|
+
context 'when HTTP status is OK' do
|
76
|
+
it 'returns true' do
|
77
|
+
response.should be_valid
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when HTTP status is not OK' do
|
82
|
+
it 'returns false' do
|
83
|
+
response.code = 403
|
84
|
+
response.should_not be_valid
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#xml' do
|
90
|
+
it 'returns a Nokogiri document' do
|
91
|
+
response.xml.should be_an_instance_of Nokogiri::XML::Document
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AmazonProduct
|
4
|
+
describe 'Synchrony adapter', :synchrony do
|
5
|
+
before(:all) do
|
6
|
+
require 'amazon_product/synchrony'
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Request, :synchrony do
|
10
|
+
let(:request) do
|
11
|
+
req = AmazonProduct['us']
|
12
|
+
req.configure do |c|
|
13
|
+
c.key = 'foo'
|
14
|
+
c.secret = 'bar'
|
15
|
+
c.tag = 'baz'
|
16
|
+
end
|
17
|
+
req
|
18
|
+
end
|
19
|
+
|
20
|
+
it "uses an evented adapter" do
|
21
|
+
request.adapter.should eql ::EM::HttpRequest
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#aget" do
|
25
|
+
it "yields a response" do
|
26
|
+
response = nil
|
27
|
+
EM.synchrony do
|
28
|
+
request.aget { |resp| response = resp }
|
29
|
+
EM.stop
|
30
|
+
end
|
31
|
+
|
32
|
+
response.should be_a Response
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#get" do
|
37
|
+
it "returns a response" do
|
38
|
+
response = nil
|
39
|
+
EM.synchrony do
|
40
|
+
response = request.get
|
41
|
+
EM.stop
|
42
|
+
end
|
43
|
+
|
44
|
+
response.should be_a Response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<?xml version="1.0" ?><ItemLookupResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2011-08-01"><OperationRequest><RequestId>86b89a15-b717-4d95-99aa-fe531b4ca762</RequestId><Arguments><Argument Name="Operation" Value="ItemLookup"></Argument><Argument Name="Service" Value="AWSECommerceService"></Argument><Argument Name="AssociateTag" Value="theorydot08-20"></Argument><Argument Name="Version" Value="2011-08-01"></Argument><Argument Name="Signature" Value="vOT9O1NW8PYLvrUX6KI3jrZ4Fg7LdtEYTrlsWzhbm1k="></Argument><Argument Name="ItemId" Value="0816614024,0143105825"></Argument><Argument Name="IdType" Value="ASIN"></Argument><Argument Name="AWSAccessKeyId" Value="0ZVSQ33MDFPQS8H2PM02"></Argument><Argument Name="Timestamp" Value="2011-07-29T17:52:34Z"></Argument></Arguments><RequestProcessingTime>0.0111990000000000</RequestProcessingTime></OperationRequest><Items><Request><IsValid>True</IsValid><ItemLookupRequest><IdType>ASIN</IdType><ItemId>0816614024</ItemId><ItemId>0143105825</ItemId><ResponseGroup>Small</ResponseGroup><VariationPage>All</VariationPage></ItemLookupRequest></Request><Item><ASIN>0816614024</ASIN><DetailPageURL>http://www.amazon.com/Thousand-Plateaus-Schizophrenia-Gilles-Deleuze/dp/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0816614024</DetailPageURL><ItemLinks><ItemLink><Description>Technical Details</Description><URL>http://www.amazon.com/Thousand-Plateaus-Schizophrenia-Gilles-Deleuze/dp/tech-data/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Baby Registry</Description><URL>http://www.amazon.com/gp/registry/baby/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Wedding Registry</Description><URL>http://www.amazon.com/gp/registry/wedding/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Wishlist</Description><URL>http://www.amazon.com/gp/registry/wishlist/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>http://www.amazon.com/gp/pdp/taf/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>http://www.amazon.com/review/product/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>http://www.amazon.com/gp/offer-listing/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink></ItemLinks><ItemAttributes><Author>Gilles Deleuze</Author><Creator Role="Contributor">Felix Guattari</Creator><Manufacturer>Univ Of Minnesota Press</Manufacturer><ProductGroup>Book</ProductGroup><Title>Thousand Plateaus: Capitalism and Schizophrenia</Title></ItemAttributes></Item><Item><ASIN>0143105825</ASIN><DetailPageURL>http://www.amazon.com/Anti-Oedipus-Capitalism-Schizophrenia-Penguin-Classics/dp/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0143105825</DetailPageURL><ItemLinks><ItemLink><Description>Technical Details</Description><URL>http://www.amazon.com/Anti-Oedipus-Capitalism-Schizophrenia-Penguin-Classics/dp/tech-data/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Baby Registry</Description><URL>http://www.amazon.com/gp/registry/baby/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Wedding Registry</Description><URL>http://www.amazon.com/gp/registry/wedding/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Wishlist</Description><URL>http://www.amazon.com/gp/registry/wishlist/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>http://www.amazon.com/gp/pdp/taf/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>http://www.amazon.com/review/product/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>http://www.amazon.com/gp/offer-listing/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink></ItemLinks><ItemAttributes><Author>Gilles Deleuze</Author><Author>Felix Guattari</Author><Creator Role="Translator">Robert Hurley</Creator><Creator Role="Translator">Mark Seem</Creator><Creator Role="Introduction">Mark Seem</Creator><Creator Role="Translator">Helen Lane</Creator><Creator Role="Preface">Michel Foucault</Creator><Manufacturer>Penguin Classics</Manufacturer><ProductGroup>Book</ProductGroup><Title>Anti-Oedipus: Capitalism and Schizophrenia (Penguin Classics)</Title></ItemAttributes></Item></Items></ItemLookupResponse>
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: amazon_product
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.0.pre.1
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Hakan Ensari
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-10 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: nokogiri
|
16
|
+
requirement: &70241913617000 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.4'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70241913617000
|
25
|
+
description: Amazon Product is a Ruby wrapper to the Amazon Product Advertising API.
|
26
|
+
email:
|
27
|
+
- code@papercavalier.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- lib/amazon_product/error.rb
|
33
|
+
- lib/amazon_product/hash_builder.rb
|
34
|
+
- lib/amazon_product/locale.rb
|
35
|
+
- lib/amazon_product/operations.rb
|
36
|
+
- lib/amazon_product/request.rb
|
37
|
+
- lib/amazon_product/response.rb
|
38
|
+
- lib/amazon_product/synchrony.rb
|
39
|
+
- lib/amazon_product/version.rb
|
40
|
+
- lib/amazon_product.rb
|
41
|
+
- LICENSE
|
42
|
+
- README.md
|
43
|
+
- spec/amazon_product/hash_builder_spec.rb
|
44
|
+
- spec/amazon_product/request_spec.rb
|
45
|
+
- spec/amazon_product/response_spec.rb
|
46
|
+
- spec/amazon_product/synchrony_spec.rb
|
47
|
+
- spec/fixtures/http_response
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
homepage: http://code.papercavalier.com/amazon_product/
|
50
|
+
licenses: []
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
hash: 1955989660319264497
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>'
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.3.1
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.8.6
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: A Ruby wrapper to the Amazon Product Advertising API
|
76
|
+
test_files:
|
77
|
+
- spec/amazon_product/hash_builder_spec.rb
|
78
|
+
- spec/amazon_product/request_spec.rb
|
79
|
+
- spec/amazon_product/response_spec.rb
|
80
|
+
- spec/amazon_product/synchrony_spec.rb
|
81
|
+
- spec/fixtures/http_response
|
82
|
+
- spec/spec_helper.rb
|