amazon_product 3.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![travis](https://secure.travis-ci.org/hakanensari/amazon_product.png)](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
|