vacuum 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93f97f47ef585594e52efc75ac3824e47992ad0ac6cbd688f05363579c9feff3
4
- data.tar.gz: 4d0399b9b897cd955cb3c2739a3b2fcb4e393c6ee092270d21d28dab8ee503fc
3
+ metadata.gz: 93d495272c66aa409a35266354596c893f93dc94f909290143909a3ed3552f8a
4
+ data.tar.gz: '0812a94c0f3254302468caa19b852e31f7e7e4650166721ed45d49f071db5303'
5
5
  SHA512:
6
- metadata.gz: eac6cca761cd2ccc75e7eb9d626398505c06400116efe5bb765e5112debb0f3b22da3dab840aa1bbde19de752d841bd836029c6c1bb7f3b12738d74bfecd0e3b
7
- data.tar.gz: 8571a8e9c4b64b71038bf7713364c8b56bca14bb08abae6743aaf2cea0a976fe1b0025432ad659b300d8d5763deb05bd9ff4200bdfcb80e52de9a2f3d3e8952c
6
+ metadata.gz: 473700d6b71e5c6c3228f64fa2e6b78c4efc7d191211a686fb14ec8426f59fa68a6c626c742e1282cd08db8df9e6a0e6ce30b3f43b991908d96fe179cc8c37fd
7
+ data.tar.gz: 43f66d7d7a0dc877a9e197006d5aa869e14b699f86554181e79794ec2bbe92846e9a2f73b320228f5c552eb8d9e278736284d947a08995e358162f444e299dbc
data/README.md CHANGED
@@ -1,20 +1,18 @@
1
1
  # Vacuum
2
2
 
3
- [![CircleCI](https://circleci.com/gh/hakanensari/vacuum/tree/master.svg?style=svg)](https://circleci.com/gh/hakanensari/vacuum/tree/master)
3
+ [![Build](https://github.com/hakanensari/vacuum/workflows/build/badge.svg)](https://github.com/hakanensari/vacuum/actions)
4
4
 
5
- Vacuum is a light-weight Ruby wrapper to [Amazon Product Advertising API 5.0](https://webservices.amazon.com/paapi5/documentation/). The API provides programmatic access to search and get detailed product information on the Amazon marketplaces.
5
+ Vacuum is a Ruby wrapper to [Amazon Product Advertising API 5.0](https://webservices.amazon.com/paapi5/documentation/). The API provides programmatic access to query product information on the Amazon marketplaces.
6
+
7
+ You need to [register first](https://webservices.amazon.com/paapi5/documentation/register-for-pa-api.html) to use the API.
6
8
 
7
9
  ![vacuum](http://f.cl.ly/items/2k2X0e2u0G3k1c260D2u/vacuum.png)
8
10
 
9
11
  ## Usage
10
12
 
11
- Vacuum follows the nomenclature of the Product Advertising API. The examples below are based on examples in the [Amazon docs](https://webservices.amazon.com/paapi5/documentation/).
12
-
13
13
  ### Getting Started
14
14
 
15
- You need to [register as an affiliate](https://affiliate-program.amazon.com) and [apply for API access](https://affiliate-program.amazon.com/assoc_credentials/home) on each marketplace you want to query product information.
16
-
17
- Create a request with your marketplace credentials, passing the two-letter country code of the marketplace.
15
+ Create a request with your marketplace credentials. Set the marketplace by passing its two-letter country code.
18
16
 
19
17
  ```ruby
20
18
  request = Vacuum.new(marketplace: 'US',
@@ -23,17 +21,26 @@ request = Vacuum.new(marketplace: 'US',
23
21
  partner_tag: '<PARTNER_TAG>')
24
22
  ```
25
23
 
26
- Vacuum uses [HTTPI](https://github.com/savonrb/httpi) under the hood. You can swap the HTTP library it uses if you prefer an alternative one for speed or introspection.
24
+ You can now access the API using the available operations.
25
+
26
+ ```ruby
27
+ response = request.search_items(title: 'lean startup')
28
+ puts response.to_h
29
+ ```
30
+
31
+ Create a persistent connection to make multiple requests.
27
32
 
28
33
  ```ruby
29
- HTTPI.adapter = :http
34
+ request.persistent
30
35
  ```
31
36
 
32
37
  ### Operations
33
38
 
39
+ Refer to the [API docs](https://webservices.amazon.com/paapi5/documentation/) for more detailed information.
40
+
34
41
  #### GetBrowseNodes
35
42
 
36
- Given a BrowseNodeId, the `GetBrowseNodes` operation returns details about the specified browse node like name, children and ancestors depending on the resources specified in the request. The names and browse node IDs of the children and ancestor browse nodes are also returned. `GetBrowseNodes` enables you to traverse the browse node hierarchy to find a browse node.
43
+ Given a BrowseNodeId, the `GetBrowseNodes` operation returns details about the specified browse node, like name, children and ancestors, depending on the resources specified in the request. The names and browse node IDs of the children and ancestor browse nodes are also returned. `GetBrowseNodes` enables you to traverse the browse node hierarchy to find a browse node.
37
44
 
38
45
  ```ruby
39
46
  request.get_browse_nodes(
@@ -69,7 +76,7 @@ request.get_variations(
69
76
 
70
77
  #### SearchItems
71
78
 
72
- The `SearchItems` operation searches for items on Amazon based on a search query. The Amazon Product Advertising API returns up to ten items per search request.
79
+ The `SearchItems` operation searches for items on Amazon based on a search query. The API returns up to ten items per search request.
73
80
 
74
81
  ```ruby
75
82
  request.search_items(keywords: 'harry potter')
@@ -77,46 +84,73 @@ request.search_items(keywords: 'harry potter')
77
84
 
78
85
  ### Response
79
86
 
80
- The quick and dirty way to consume a response is to parse into a Ruby hash:
87
+ Consume a response by parsing it into a Ruby hash.
81
88
 
82
89
  ```ruby
83
90
  response.to_h
84
91
  ```
85
92
 
86
- You can also `#dig` into the returned Hash:
93
+ You can also `#dig` into this hash.
87
94
 
88
95
  ```ruby
89
96
  response.dig('ItemsResult', 'Items')
90
97
  ```
91
98
 
92
- You can extend Vacuum with a custom parser. Just swap the original with a class or module that responds to `.parse`.
99
+ ### Troubleshooting
100
+
101
+ In addition to the response payload, the following attributes help you introspect a request.
93
102
 
94
103
  ```ruby
95
- response.parser = MyParser
96
- response.parse
104
+ request.body
105
+ request.headers
106
+ request.url
97
107
  ```
98
108
 
99
- If no custom parser is set, `Vacuum::Response#parse` delegates to `#to_h`.
109
+ ### VCR
100
110
 
101
- ### VCR Support
111
+ If you are using [VCR](https://github.com/vcr/vcr) to test an app that accesses the API, you can use the custom VCR matcher of Vacuum to stub requests.
112
+
113
+ ```ruby
114
+ require 'vacuum/matcher'
115
+
116
+ # in your test
117
+ VCR.insert_cassette('cassette_name',
118
+ match_requests_on: [Vacuum::Matcher])
119
+ ```
102
120
 
103
- If you are using [VCR](https://github.com/vcr/vcr) to test an app that accesses the Product Advertising API, you can use the custom VCR matcher of Vacuum to stub requests.
121
+ In RSpec, consider using custom metadata.
104
122
 
105
123
  ```ruby
106
124
  require 'vacuum/matcher'
107
125
 
108
- VCR.insert_cassette('paapi', match_requests_on: [Vacuum::Matcher])
126
+ RSpec.configure do |config|
127
+ config.around do |example|
128
+ if example.metadata[:paapi]
129
+ metadata = example.metadata[:paapi]
130
+ metadata = {} if metadata == true
131
+ example.metadata[:vcr] = metadata.merge(
132
+ match_requests_on: [Vacuum::Matcher]
133
+ )
134
+ end
135
+
136
+ example.run
137
+ end
138
+ end
139
+
140
+ # in your test
141
+ it 'queries Amazon', :paapi do
142
+ end
109
143
  ```
110
144
 
111
- ## Testing
145
+ ## Development
112
146
 
113
- Tests should pass as-is once you install dependencies.
147
+ Clone the repo and install dependencies. Tests should pass as-is.
114
148
 
115
149
  ```sh
116
150
  bundle exec rake
117
151
  ```
118
152
 
119
- By default, all requests are stubbed. Use the `RECORD` env var to record new or modified interactions.
153
+ By default, all requests are stubbed. Use the `RECORD` env var to record new interactions.
120
154
 
121
155
  ```sh
122
156
  bundle exec RECORD=true rake
@@ -128,7 +162,7 @@ You can also run tests against live data.
128
162
  bundle exec LIVE=true rake
129
163
  ```
130
164
 
131
- In either case, you will want to add actual API credentials to a [`locales.yml`](https://github.com/hakanensari/vacuum/blob/master/test/locales.yml.example) file in the `test` directory.
165
+ In either case, add actual API credentials to a [`locales.yml`](https://github.com/hakanensari/vacuum/blob/master/test/locales.yml.example) file under `test`.
132
166
 
133
167
  ## Getting Help
134
168
 
data/lib/vacuum.rb CHANGED
@@ -10,6 +10,11 @@ module Vacuum
10
10
  class << self
11
11
  extend Forwardable
12
12
 
13
+ # @!method new
14
+ # Delegates to {Request} to create a new request
15
+ #
16
+ # @return [Request]
17
+ # @see Request#initialize
13
18
  def_delegator 'Vacuum::Request', :new
14
19
  end
15
20
  end
data/lib/vacuum/locale.rb CHANGED
@@ -1,53 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vacuum
4
- # The target Amazon locale
5
- # @api private
4
+ # The target locale
5
+ #
6
+ # @see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
6
7
  class Locale
7
- class NotFound < ArgumentError; end
8
-
9
- attr_reader :code, :domain, :region
10
-
11
- def self.find(code)
12
- code = code.to_sym.downcase
13
- code = :gb if code == :uk
14
-
15
- @all.find { |locale| locale.code == code } || raise(NotFound)
16
- end
17
-
18
- def initialize(code, domain, region)
19
- @code = code
20
- @domain = domain
21
- @region = region
8
+ # Raised when the provided marketplace does not correspond to an existing
9
+ # Amazon locale
10
+ class NotFound < KeyError; end
11
+
12
+ # @!visibility private
13
+ HOSTS_AND_REGIONS = {
14
+ au: ['webservices.amazon.com.au', 'us-west-2'],
15
+ br: ['webservices.amazon.com.br', 'us-east-1'],
16
+ ca: ['webservices.amazon.ca', 'us-east-1'],
17
+ fr: ['webservices.amazon.fr', 'eu-west-1'],
18
+ de: ['webservices.amazon.de', 'eu-west-1'],
19
+ in: ['webservices.amazon.in', 'eu-west-1'],
20
+ it: ['webservices.amazon.it', 'eu-west-1'],
21
+ jp: ['webservices.amazon.co.jp', 'us-west-2'],
22
+ mx: ['webservices.amazon.com.mx', 'us-east-1'],
23
+ es: ['webservices.amazon.es', 'eu-west-1'],
24
+ tr: ['webservices.amazon.com.tr', 'eu-west-1'],
25
+ ae: ['webservices.amazon.ae', 'eu-west-1'],
26
+ gb: ['webservices.amazon.co.uk', 'eu-west-1'],
27
+ us: ['webservices.amazon.com', 'us-east-1']
28
+ }.freeze
29
+
30
+ # @return [String]
31
+ attr_reader :host, :region, :access_key, :secret_key, :partner_tag,
32
+ :partner_type
33
+
34
+ # Creates a locale
35
+ #
36
+ # @param [Symbol,String] marketplace
37
+ # @param [String] access_key
38
+ # @param [String] secret_key
39
+ # @param [String] partner_tag
40
+ # @param [String] partner_type
41
+ # @raise [NotFound] if marketplace is not found
42
+ def initialize(marketplace, access_key:, secret_key:, partner_tag:,
43
+ partner_type: 'Associates')
44
+ @host, @region = find_host_and_region(marketplace)
45
+ @access_key = access_key
46
+ @secret_key = secret_key
47
+ @partner_tag = partner_tag
48
+ @partner_type = partner_type
22
49
  end
23
50
 
24
- def endpoint
25
- "webservices.#{domain}"
26
- end
51
+ private
27
52
 
28
- def marketplace
29
- "www.#{domain}"
30
- end
53
+ def find_host_and_region(marketplace)
54
+ marketplace = marketplace.to_sym.downcase
55
+ marketplace = :gb if marketplace == :uk
31
56
 
32
- def build_url(operation)
33
- "https://#{endpoint}/paapi5/#{operation.downcase}"
57
+ HOSTS_AND_REGIONS.fetch(marketplace)
58
+ rescue KeyError
59
+ raise NotFound, "marketplace not found: :#{marketplace}"
34
60
  end
35
-
36
- @all = [
37
- [:au, 'amazon.com.au', 'us-west-2'],
38
- [:br, 'amazon.com.br', 'us-east-1'],
39
- [:ca, 'amazon.ca', 'us-east-1'],
40
- [:fr, 'amazon.fr', 'eu-west-1'],
41
- [:de, 'amazon.de', 'eu-west-1'],
42
- [:in, 'amazon.in', 'eu-west-1'],
43
- [:it, 'amazon.it', 'eu-west-1'],
44
- [:jp, 'amazon.co.jp', 'us-west-2'],
45
- [:mx, 'amazon.com.mx', 'us-east-1'],
46
- [:es, 'amazon.es', 'eu-west-1'],
47
- [:tr, 'amazon.com.tr', 'eu-west-1'],
48
- [:ae, 'amazon.ae', 'eu-west-1'],
49
- [:gb, 'amazon.co.uk', 'eu-west-1'],
50
- [:us, 'amazon.com', 'us-east-1']
51
- ].map { |attributes| new(*attributes) }
52
61
  end
53
62
  end
@@ -4,20 +4,35 @@ require 'json'
4
4
 
5
5
  module Vacuum
6
6
  # Custom VCR matcher for stubbing calls to the Product Advertising API
7
- # @api private
7
+ #
8
+ # The matcher is not required by default.
9
+ #
10
+ # @example
11
+ # require 'vacuum/matcher'
12
+ #
13
+ # # in your test
14
+ # VCR.insert_cassette('cassette_name',
15
+ # match_requests_on: [Vacuum::Matcher])
16
+ #
17
+ # @see https://relishapp.com/vcr/vcr/v/5-0-0/docs/request-matching/register-and-use-a-custom-matcher
8
18
  class Matcher
9
19
  IGNORED_KEYS = %w[PartnerTag].freeze
20
+ private_constant :IGNORED_KEYS
10
21
 
22
+ # @!visibility private
11
23
  attr_reader :requests
12
24
 
25
+ # @!visibility private
13
26
  def self.call(*requests)
14
27
  new(*requests).compare
15
28
  end
16
29
 
30
+ # @!visibility private
17
31
  def initialize(*requests)
18
32
  @requests = requests
19
33
  end
20
34
 
35
+ # @!visibility private
21
36
  def compare
22
37
  uris.reduce(:==) && bodies.reduce(:==)
23
38
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sigv4'
4
+ require 'http'
5
+ require 'json'
6
+
7
+ module Vacuum
8
+ # An Amazon Product Advertising API operation
9
+ class Operation
10
+ # @!visibility private
11
+ attr_reader :locale, :name, :params
12
+
13
+ # Creates a new operation
14
+ #
15
+ # @param [String] name
16
+ # @param [Hash] params
17
+ # @param [Locale] locale
18
+ def initialize(name, params:, locale:)
19
+ @name = name
20
+ @params = params
21
+ @locale = locale
22
+ end
23
+
24
+ # @return [Hash]
25
+ def headers
26
+ signature.headers.merge(
27
+ 'x-amz-target' =>
28
+ "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.#{name}",
29
+ 'content-encoding' => 'amz-1.0',
30
+ 'content-type' => 'application/json; charset=utf-8'
31
+ )
32
+ end
33
+
34
+ # @return [String]
35
+ def body
36
+ @body ||= build_body
37
+ end
38
+
39
+ # @return [String]
40
+ def url
41
+ @url ||= build_url
42
+ end
43
+
44
+ private
45
+
46
+ def build_body
47
+ hsh = { 'PartnerTag' => locale.partner_tag,
48
+ 'PartnerType' => locale.partner_type }
49
+
50
+ params.each do |key, val|
51
+ key = key.to_s.split('_')
52
+ .map { |word| word == 'asin' ? 'ASIN' : word.capitalize }.join
53
+ hsh[key] = val
54
+ end
55
+
56
+ JSON.generate(hsh)
57
+ end
58
+
59
+ def build_url
60
+ "https://#{locale.host}/paapi5/#{name.downcase}"
61
+ end
62
+
63
+ def signature
64
+ signer.sign_request(http_method: 'POST', url: url, body: body)
65
+ end
66
+
67
+ def signer
68
+ Aws::Sigv4::Signer.new(service: 'ProductAdvertisingAPI',
69
+ region: locale.region,
70
+ access_key_id: locale.access_key,
71
+ secret_access_key: locale.secret_key,
72
+ http_method: 'POST', endpoint: locale.host)
73
+ end
74
+ end
75
+ end
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aws-sigv4'
4
- require 'httpi'
5
- require 'json'
3
+ require 'http'
6
4
 
7
5
  require 'vacuum/locale'
6
+ require 'vacuum/operation'
8
7
  require 'vacuum/response'
9
8
 
10
9
  module Vacuum
11
10
  # A request to the Amazon Product Advertising API
12
11
  class Request
13
- SERVICE = 'ProductAdvertisingAPI'
14
- private_constant :SERVICE
12
+ # @return [HTTP::Client]
13
+ attr_reader :client
15
14
 
16
- # @api private
17
- attr_reader :access_key, :secret_key, :locale, :partner_tag, :partner_type
15
+ # @return [Locale]
16
+ attr_reader :locale
17
+
18
+ # @return [Operation]
19
+ attr_reader :operation
18
20
 
19
21
  # Creates a new request
20
- # @param [Symbol,String] marketplace the two-letter country code of the
21
- # target Amazon locale
22
- # @param [String] access_key your access key
23
- # @param [String] secret_key your secret key
24
- # @param [String] partner_tag your partner tag
25
- # @param [String] partner_type your partner type
26
- def initialize(marketplace: :us, access_key:, secret_key:, partner_tag:,
27
- partner_type: 'Associates')
28
- @locale = Locale.find(marketplace)
29
- @access_key = access_key
30
- @secret_key = secret_key
31
- @partner_tag = partner_tag
32
- @partner_type = partner_type
22
+ #
23
+ # @overload initialize(marketplace: :us, access_key:, secret_key:, partner_tag:, partner_type:)
24
+ # @param [Symbol,String] marketplace
25
+ # @param [String] access_key
26
+ # @param [String] secret_key
27
+ # @param [String] partner_tag
28
+ # @param [String] partner_type
29
+ # @raise [Locale::NotFound] if marketplace is not found
30
+ def initialize(marketplace: :us, **args)
31
+ @locale = Locale.new(marketplace, args)
32
+ @client = HTTP::Client.new
33
33
  end
34
34
 
35
35
  # Returns details about specified browse nodes
@@ -119,69 +119,22 @@ module Vacuum
119
119
  request('SearchItems', params)
120
120
  end
121
121
 
122
- private
123
-
124
- def request(operation, params)
125
- body = build_body(params)
126
- signature = sign(operation, body)
127
-
128
- request = HTTPI::Request.new(
129
- headers: request_headers(operation, signature),
130
- url: locale.build_url(operation),
131
- body: body
132
- )
133
-
134
- Response.new(HTTPI.post(request))
135
- end
136
-
137
- def sign(operation, body)
138
- signer.sign_request(
139
- http_method: 'POST',
140
- url: locale.build_url(operation),
141
- headers: headers(operation),
142
- body: body
143
- )
122
+ # Creates a persistent connection for multiple requests
123
+ #
124
+ # @return [self]
125
+ def persistent
126
+ @client = client.persistent("https://#{locale.host}")
127
+ self
144
128
  end
145
129
 
146
- def request_headers(operation, signature)
147
- headers(operation).merge(
148
- 'Content-Type' => 'application/json; charset=utf-8',
149
- 'Authorization' => signature.headers['authorization'],
150
- 'X-Amz-Content-Sha256' => signature.headers['x-amz-content-sha256'],
151
- 'X-Amz-Date' => signature.headers['x-amz-date'],
152
- 'Host' => locale.endpoint
153
- )
154
- end
155
-
156
- def headers(operation)
157
- {
158
- 'X-Amz-Target' => "com.amazon.paapi5.v1.#{SERVICE}v1.#{operation}",
159
- 'Content-Encoding' => 'amz-1.0'
160
- }
161
- end
162
-
163
- def signer
164
- Aws::Sigv4::Signer.new(
165
- service: SERVICE,
166
- region: locale.region,
167
- access_key_id: access_key,
168
- secret_access_key: secret_key,
169
- http_method: 'POST',
170
- endpoint: locale.endpoint
171
- )
172
- end
173
-
174
- def build_body(params)
175
- hsh = { 'PartnerTag' => partner_tag,
176
- 'PartnerType' => partner_type }
130
+ private
177
131
 
178
- params.each do |key, val|
179
- key = key.to_s.split('_')
180
- .map { |word| word == 'asin' ? 'ASIN' : word.capitalize }.join
181
- hsh[key] = val
182
- end
132
+ def request(operation_name, params)
133
+ @operation = Operation.new(operation_name, params: params, locale: locale)
134
+ response = client.headers(operation.headers)
135
+ .post(operation.url, body: operation.body)
183
136
 
184
- JSON.generate(hsh)
137
+ Response.new(response)
185
138
  end
186
139
  end
187
140
  end