vacuum 2.2.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,122 +1,189 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jeff'
3
+ require 'http'
4
+
5
+ require 'vacuum/locale'
6
+ require 'vacuum/operation'
7
+ require 'vacuum/resource'
4
8
  require 'vacuum/response'
5
9
 
6
10
  module Vacuum
7
- # An Amazon Product Advertising API request.
11
+ # A request to the Amazon Product Advertising API
8
12
  class Request
9
- include Jeff
10
-
11
- BadLocale = Class.new(ArgumentError)
12
-
13
- LATEST_VERSION = '2013-08-01'
14
-
15
- HOSTS = {
16
- 'AU' => 'webservices.amazon.com.au',
17
- 'BR' => 'webservices.amazon.com.br',
18
- 'CA' => 'webservices.amazon.ca',
19
- 'CN' => 'webservices.amazon.cn',
20
- 'DE' => 'webservices.amazon.de',
21
- 'ES' => 'webservices.amazon.es',
22
- 'FR' => 'webservices.amazon.fr',
23
- 'GB' => 'webservices.amazon.co.uk',
24
- 'IN' => 'webservices.amazon.in',
25
- 'IT' => 'webservices.amazon.it',
26
- 'JP' => 'webservices.amazon.co.jp',
27
- 'MX' => 'webservices.amazon.com.mx',
28
- 'TR' => 'webservices.amazon.com.tr',
29
- 'US' => 'webservices.amazon.com'
30
- }.freeze
31
-
32
- OPERATIONS = %w[
33
- BrowseNodeLookup
34
- CartAdd
35
- CartClear
36
- CartCreate
37
- CartGet
38
- CartModify
39
- ItemLookup
40
- ItemSearch
41
- SimilarityLookup
42
- ].freeze
43
- private_constant :OPERATIONS
44
-
45
- params 'AssociateTag' => -> { associate_tag },
46
- 'Service' => 'AWSECommerceService',
47
- 'SubscriptionId' => -> { subscription_id },
48
- 'Version' => -> { version }
49
-
50
- attr_accessor :associate_tag, :subscription_id
51
- attr_writer :version
52
-
53
- # Create a new request for given locale.
54
- #
55
- # locale - The String Product Advertising API locale (default: US).
56
- # secure - Whether to use the secure version of the endpoint (default:
57
- # false)
13
+ # @return [HTTP::Client]
14
+ attr_reader :client
15
+
16
+ # @return [Locale]
17
+ attr_reader :locale
18
+
19
+ # @return [Operation]
20
+ attr_reader :operation
21
+
22
+ # Creates a new request
58
23
  #
59
- # Raises a Bad Locale error if locale is not valid.
60
- def initialize(locale = 'US', secure = false)
61
- locale = 'GB' if locale == 'UK'
62
- host = HOSTS.fetch(locale) { raise BadLocale }
63
- @aws_endpoint = "#{secure ? 'https' : 'http'}://#{host}/onca/xml"
24
+ # @overload initialize(marketplace: :us, access_key:, secret_key:, partner_tag:, partner_type:)
25
+ # @param [Symbol,String] marketplace
26
+ # @param [String] access_key
27
+ # @param [String] secret_key
28
+ # @param [String] partner_tag
29
+ # @param [String] partner_type
30
+ # @raise [Locale::NotFound] if marketplace is not found
31
+ def initialize(marketplace: :us, **args)
32
+ @locale = Locale.new(marketplace, **args)
33
+ @client = HTTP::Client.new
64
34
  end
65
35
 
66
- # Configure the Amazon Product Advertising API request.
67
- #
68
- # credentials - The Hash credentials of the API endpoint.
69
- # :aws_access_key_id - The String Amazon Web Services
70
- # (AWS) key.
71
- # :aws_secret_access_key - The String AWS secret.
72
- # :associate_tag - The String Associate Tag.
73
- # :aws_version - The String AWS version.
36
+ # Returns details about specified browse nodes
74
37
  #
75
- # Returns self.
76
- def configure(credentials)
77
- credentials.each { |key, val| send("#{key}=", val) }
78
- self
38
+ # @see https://webservices.amazon.com/paapi5/documentation/getbrowsenodes.html
39
+ # @overload get_browse_nodes(browse_node_ids:, languages_of_preference: nil, marketplace: nil, partner_tag: nil, partner_type: nil, resources: nil)
40
+ # @param [Array<String,Integer>,String,Integer] browse_node_ids
41
+ # @param [Array<String>,nil] languages_of_preference
42
+ # @param [String,nil] marketplace
43
+ # @param [String,nil] partner_tag
44
+ # @param [String,nil] partner_type
45
+ # @param [Array<String>,nil] resources
46
+ # @return [Response]
47
+ def get_browse_nodes(browse_node_ids:, **params)
48
+ params.update(browse_node_ids: Array(browse_node_ids))
49
+ request('GetBrowseNodes', params)
79
50
  end
80
51
 
81
- # Returns the API version.
82
- def version
83
- @version || LATEST_VERSION
52
+ # Returns the attributes of one or more items
53
+ #
54
+ # @see https://webservices.amazon.com/paapi5/documentation/get-items.html
55
+ # @overload get_items(condition: nil, currency_of_preference: nil, item_id_type: nil, item_ids:, languages_of_preference: nil, marketplace: nil, merchant: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil)
56
+ # @param [String,nil] condition
57
+ # @param [String,nil] currency_of_preference
58
+ # @param [String,nil] item_id_type
59
+ # @param [Array<String>,String] item_ids
60
+ # @param [Array<String>,nil] languages_of_preference
61
+ # @param [String,nil] marketplace
62
+ # @param [String,nil] merchant
63
+ # @param [Integer,nil] offer_count
64
+ # @param [String,nil] partner_tag
65
+ # @param [String,nil] partner_type
66
+ # @param [Array<String>,nil] resources
67
+ # @return [Response]
68
+ def get_items(item_ids:, **params)
69
+ params.update(item_ids: Array(item_ids))
70
+ request('GetItems', params)
84
71
  end
85
72
 
86
- # Execute an API operation. See `OPERATIONS` constant above for available
87
- # operation names.
73
+ # Returns a set of items that are the same product, but differ according to
74
+ # a consistent theme
88
75
  #
89
- # params - The Hash request parameters.
90
- # opts - Options passed to Excon (default: {}).
76
+ # @see https://webservices.amazon.com/paapi5/documentation/get-variations.html
77
+ # @overload get_variations(asin:, condition: nil, currency_of_preference: nil, languages_of_preference: nil, marketplace: nil, merchant: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil, variation_count: nil, variation_page: nil)
78
+ # @param [String] asin
79
+ # @param [String,nil] condition
80
+ # @param [String,nil] currency_of_preference
81
+ # @param [Array<String>,nil] languages_of_preference
82
+ # @param [String,nil] marketplace
83
+ # @param [String,nil] merchant
84
+ # @param [Integer,nil] offer_count
85
+ # @param [String,nil] partner_tag
86
+ # @param [String,nil] partner_type
87
+ # @param [Array<String>,nil] resources
88
+ # @param [Integer,nil] variation_count
89
+ # @param [Integer,nil] variation_page
90
+ # @return [Response]
91
+ def get_variations(**params)
92
+ request('GetVariations', params)
93
+ end
94
+
95
+ # Searches for items on Amazon based on a search query
91
96
  #
92
- # Alternatively, pass Excon options as first argument and include request
93
- # parameters as query key.
97
+ # @see https://webservices.amazon.com/paapi5/documentation/search-items.html
98
+ # @overload search_items(actor: nil, artist: nil, author: nil, availability: nil, brand: nil, browse_node_id: nil, condition: nil, currency_of_preference: nil, delivery_flags: nil, item_count: nil, item_page: nil, keywords: nil, languages_of_preference: nil, marketplace: nil, max_price: nil, merchant: nil, min_price: nil, min_reviews_rating: nil, min_savings_percent: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil, search_index: nil, sort_by: nil, title: nil)
99
+ # @param [String,nil] actor
100
+ # @param [String,nil] artist
101
+ # @param [String,nil] availability
102
+ # @param [String,nil] brand
103
+ # @param [Integer,nil] browse_node_id
104
+ # @param [String,nil] condition
105
+ # @param [String,nil] currency_of_preference
106
+ # @param [Array<String>,nil] delivery_flags
107
+ # @param [Integer,nil] item_count
108
+ # @param [Integer,nil] item_page
109
+ # @param [String,nil] keywords
110
+ # @param [Array<String>,nil] languages_of_preference
111
+ # @param [Integer,nil] max_price
112
+ # @param [String,nil] merchant
113
+ # @param [Integer,nil] min_price
114
+ # @param [Integer,nil] min_reviews_rating
115
+ # @param [Integer,nil] min_savings_percent
116
+ # @param [Integer,nil] offer_count
117
+ # @param [Hash,nil] properties
118
+ # @param [Array<String>,nil] resources
119
+ # @param [String,nil] search_index
120
+ # @param [String,nil] sort_by
121
+ # @param [String,nil] title
122
+ # @return [Response]
123
+ def search_items(**params)
124
+ request('SearchItems', params)
125
+ end
126
+
127
+ # Flags as persistent
94
128
  #
95
- # Examples
129
+ # @param [Integer] timeout
130
+ # @return [self]
131
+ def persistent(timeout: 5)
132
+ host = "https://#{locale.host}"
133
+ @client = client.persistent(host, timeout: timeout)
134
+
135
+ self
136
+ end
137
+
138
+ # @!method use(*features)
139
+ # Turn on {https://github.com/httprb/http HTTP} features
96
140
  #
97
- # req.item_search(
98
- # 'SearchIndex' => 'All',
99
- # 'Keywords' => 'Architecture'
100
- # )
141
+ # @param features
142
+ # @return [self]
101
143
  #
102
- # req.item_search(
103
- # query: {
104
- # 'SearchIndex' => 'All',
105
- # 'Keywords' => 'Architecture'
106
- # },
107
- # persistent: true
108
- # )
144
+ # @!method via(*proxy)
145
+ # Make a request through an HTTP proxy
109
146
  #
110
- # Returns a Vacuum Response.
111
- OPERATIONS.each do |operation|
112
- method_name = operation.gsub(/(.)([A-Z])/, '\1_\2').downcase
113
- define_method(method_name) do |params, opts = {}|
114
- params.key?(:query) ? opts = params : opts.update(query: params)
115
- opts[:expects] ||= [200, 400, 403]
116
- opts[:query].update('Operation' => operation)
117
-
118
- Response.new(get(opts))
147
+ # @param [Array] proxy
148
+ # @raise [HTTP::Request::Error] if HTTP proxy is invalid
149
+ # @return [self]
150
+ %i[timeout via through headers use].each do |method_name|
151
+ define_method(method_name) do |*args, &block|
152
+ @client = client.send(method_name, *args, &block)
119
153
  end
120
154
  end
155
+
156
+ private
157
+
158
+ def validate(params)
159
+ validate_keywords(params)
160
+ validate_resources(params)
161
+ end
162
+
163
+ def validate_keywords(params)
164
+ return unless params[:keywords]
165
+ return if params[:keywords].is_a?(String)
166
+
167
+ raise ArgumentError, ':keyword argument expects a String'
168
+ end
169
+
170
+ def validate_resources(params)
171
+ return unless params[:resources]
172
+
173
+ raise ArgumentError, ':resources argument expects an Array' unless params[:resources].is_a?(Array)
174
+
175
+ params[:resources].each do |resource|
176
+ raise ArgumentError, "There is not such resource: #{resource}" unless Resource.valid?(resource)
177
+ end
178
+ end
179
+
180
+ def request(operation_name, params)
181
+ validate(params)
182
+ @operation = Operation.new(operation_name, params: params, locale: locale)
183
+ response = client.headers(operation.headers)
184
+ .post(operation.url, body: operation.body)
185
+
186
+ Response.new(response)
187
+ end
121
188
  end
122
189
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vacuum
4
+ # Resources determine what information will be returned in the API response
5
+ #
6
+ # @see https://webservices.amazon.com/paapi5/documentation/resources.html
7
+ class Resource
8
+ ALL = %w[BrowseNodeInfo.BrowseNodes
9
+ BrowseNodeInfo.BrowseNodes.Ancestor
10
+ BrowseNodeInfo.BrowseNodes.SalesRank
11
+ BrowseNodeInfo.WebsiteSalesRank
12
+ Images.Primary.Small
13
+ Images.Primary.Medium
14
+ Images.Primary.Large
15
+ Images.Variants.Small
16
+ Images.Variants.Medium
17
+ Images.Variants.Large
18
+ ItemInfo.ByLineInfo
19
+ ItemInfo.Classifications
20
+ ItemInfo.ContentInfo
21
+ ItemInfo.ContentRating
22
+ ItemInfo.ExternalIds
23
+ ItemInfo.Features
24
+ ItemInfo.ManufactureInfo
25
+ ItemInfo.ProductInfo
26
+ ItemInfo.TechnicalInfo
27
+ ItemInfo.Title
28
+ ItemInfo.TradeInInfo
29
+ Offers.Listings.Availability.MaxOrderQuantity
30
+ Offers.Listings.Availability.Message
31
+ Offers.Listings.Availability.MinOrderQuantity
32
+ Offers.Listings.Availability.Type
33
+ Offers.Listings.Condition
34
+ Offers.Listings.Condition.SubCondition
35
+ Offers.Listings.DeliveryInfo.IsAmazonFulfilled
36
+ Offers.Listings.DeliveryInfo.IsFreeShippingEligible
37
+ Offers.Listings.DeliveryInfo.IsPrimeEligible
38
+ Offers.Listings.IsBuyBoxWinner
39
+ Offers.Listings.LoyaltyPoints.Points
40
+ Offers.Listings.MerchantInfo
41
+ Offers.Listings.Price
42
+ Offers.Listings.ProgramEligibility.IsPrimeExclusive
43
+ Offers.Listings.ProgramEligibility.IsPrimePantry
44
+ Offers.Listings.Promotions
45
+ Offers.Listings.SavingBasis
46
+ Offers.Summaries.HighestPrice
47
+ Offers.Summaries.LowestPrice
48
+ Offers.Summaries.OfferCount
49
+ ParentASIN].freeze
50
+ private_constant :ALL
51
+
52
+ # @!attribute [r] all
53
+ # @return [Array<String>]
54
+ def self.all
55
+ ALL
56
+ end
57
+
58
+ def self.valid?(resource)
59
+ ALL.include?(resource)
60
+ end
61
+ end
62
+ end
@@ -2,35 +2,47 @@
2
2
 
3
3
  require 'delegate'
4
4
  require 'forwardable'
5
- require 'multi_xml'
5
+ require 'json'
6
6
 
7
7
  module Vacuum
8
- # A wrapper around the Amazon Product Advertising API response.
8
+ # A wrapper around the API response
9
9
  class Response < SimpleDelegator
10
10
  extend Forwardable
11
11
 
12
+ # @!method dig(*key)
13
+ # Delegates to the Hash returned by {Response#to_h} to extract a nested
14
+ # value specified by the sequence of keys
15
+ #
16
+ # @param [String] key one or more keys
17
+ # @see https://ruby-doc.org/core/Hash.html#method-i-dig
18
+ def_delegator :to_h, :dig
19
+
12
20
  class << self
21
+ # @return [nil,.parse] an optional custom parser
13
22
  attr_accessor :parser
14
23
  end
15
24
 
16
- def_delegator :to_h, :dig
17
-
25
+ # @return [nil,.parse] an optional custom parser
18
26
  attr_writer :parser
19
27
 
28
+ # @!attribute [r] parser
29
+ # @return [nil,.parse] an optional custom parser
20
30
  def parser
21
31
  @parser || self.class.parser
22
32
  end
23
33
 
34
+ # Parses the response body
35
+ #
36
+ # @note Delegates to {#to_h} if no custom parser is set
24
37
  def parse
25
38
  parser ? parser.parse(body) : to_h
26
39
  end
27
40
 
41
+ # Casts body to Hash
42
+ #
43
+ # @return [Hash]
28
44
  def to_h
29
- MultiXml.parse(body)
30
- end
31
-
32
- def body
33
- (+__getobj__.body).force_encoding(Encoding::UTF_8)
45
+ JSON.parse(body)
34
46
  end
35
47
  end
36
48
  end