Skip to content

Refactor Amazon Product API endpoints #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ account or login locally:

By the end of this section, you should be able to search Amazon products and
add items to wishlists on your local machine (when your logged-in user is an
admin or site manager).
admin or site manager). *Note: if you're adding a new API endpoint, read more
[here][API client README].*

[API client README]: lib/amazon_product_api/README.md

**This step is only required for site managers and admins searching/adding
Amazon products.** If your issue doesn't involve working with the Amazon
Expand Down
68 changes: 68 additions & 0 deletions lib/amazon_product_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Amazon Product Advertising API Client

This folder contains the wrapper code for the Amazon Product Advertising API.
For more details on the API, check the [Amazon documentation].

[Amazon documentation]: http://docs.aws.amazon.com/AWSECommerceService/latest/DG/Welcome.html

## Adding an Endpoint

All endpoints should be subclassed from `AmazonProductAPI::Endpoint`. In order
to add a new endpoint, you'll need to modify the template below in a few ways:

* Prepend any new attributes to the `#initialize` parameter list. Any
validations or processing can be done in `#initialize` as shown. Note:
setting `aws_credentials` is **required**!

* Add endpoint-specific request parameters to `#request_params`. These can
be found in the Amazon API documentation.

* Add any post-processing of the API response in `#process_response`.

* Update the class name and comments.

### Endpoint Template

```ruby
require 'amazon_product_api/endpoint'

module AmazonProductAPI
# Responsible for building and executing <...>
#
# <endpoint url>
#
# Contains all specialization logic for this endpoint including request
# parameters, parameter validation, and response parsing.
class TemplateEndpoint < Endpoint
# Add any parameters you need for the specific endpoint.
#
# Make sure you set `@aws_credentials`-the query won't work without it!
def initialize(aws_credentials)
# Attribute validations
# raise InvalidQueryError, 'reason' if ...

# Initialize attributes
@aws_credentials = aws_credentials
end

private

attr_accessor :aws_credentials # any other attrs

# Add any post-processing of the response hash.
def process_response(response_hash)
ExampleResponse.new(response_hash).item
end

# Include request parameters unique to this endpoint.
def request_params
{
# 'Operation' => 'ItemLookup',
# 'IdType' => 'ASIN',
# 'ItemId' => 'the item asin',
# ...
}
end
end
end
```
104 changes: 104 additions & 0 deletions lib/amazon_product_api/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

module AmazonProductAPI
# Base representation of all Amazon Product Advertising API endpoints.
#
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\
# CHAP_OperationListAlphabetical.html
#
# Any general logic relating to lookup, building the query string,
# authentication signatures, etc. should live in this class. Specializations
# (including specific request parameters and response parsing) should live in
# endpoint subclasses.
class Endpoint
require 'httparty'
require 'time'
require 'uri'
require 'openssl'
require 'base64'

# The region you are interested in
ENDPOINT = 'webservices.amazon.com'
REQUEST_URI = '/onca/xml'

# Generates the signed URL
def url
raise InvalidQueryError, 'Missing AWS credentials' unless aws_credentials

"http://#{ENDPOINT}#{REQUEST_URI}" + # base
"?#{canonical_query_string}" + # query
"&Signature=#{uri_escape(signature)}" # signature
end

# Sends the HTTP request
def get(http: HTTParty)
http.get(url)
end

# Performs the search query and returns the processed response
def response(http: HTTParty, logger: Rails.logger)
response = parse_response get(http: http)
logger.debug response
process_response(response)
end

private

attr_reader :aws_credentials

# Takes the response hash and returns the processed API response
#
# This must be implemented for each individual endpoint.
def process_response(_response_hash)
raise NotImplementedError, 'Implement this method in your subclass.'
end

# Returns a hash of request parameters unique to the endpoint
#
# This must be implemented for each individual endpoint.
def request_params
raise NotImplementedError, 'Implement this method in your subclass.'
end

def params
params = request_params.merge(
'Service' => 'AWSECommerceService',
'AWSAccessKeyId' => aws_credentials.access_key,
'AssociateTag' => aws_credentials.associate_tag
)

# Set current timestamp if not set
params['Timestamp'] ||= Time.now.gmtime.iso8601
params
end

def parse_response(response)
Hash.from_xml(response.body)
end

# Generates the signature required by the Product Advertising API
def signature
Base64.encode64(digest_with_key(string_to_sign)).strip
end

def string_to_sign
"GET\n#{ENDPOINT}\n#{REQUEST_URI}\n#{canonical_query_string}"
end

def canonical_query_string
params.sort
.map { |key, value| "#{uri_escape(key)}=#{uri_escape(value)}" }
.join('&')
end

def digest_with_key(string)
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
aws_credentials.secret_key,
string)
end

def uri_escape(phrase)
CGI.escape(phrase.to_s)
end
end
end
85 changes: 13 additions & 72 deletions lib/amazon_product_api/item_lookup_endpoint.rb
Original file line number Diff line number Diff line change
@@ -1,98 +1,39 @@
# frozen_string_literal: true

require 'amazon_product_api/endpoint'
require 'amazon_product_api/lookup_response'

module AmazonProductAPI
# Responsible for looking up an item listing on Amazon
#
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html
#
# Any logic relating to lookup, building the query string, authentication
# signatures, etc. should live in this class.
class ItemLookupEndpoint
require 'httparty'
require 'time'
require 'uri'
require 'openssl'
require 'base64'

# The region you are interested in
ENDPOINT = 'webservices.amazon.com'
REQUEST_URI = '/onca/xml'

attr_accessor :asin, :aws_credentials

# Contains all specialization logic for this endpoint including request
# parameters, parameter validation, and response parsing.
class ItemLookupEndpoint < Endpoint
def initialize(asin, aws_credentials)
@asin = asin
@aws_credentials = aws_credentials
end

# Generate the signed URL
def url
"http://#{ENDPOINT}#{REQUEST_URI}" + # base
"?#{canonical_query_string}" + # query
"&Signature=#{uri_escape(signature)}" # signature
end

# Send the HTTP request
def get(http: HTTParty)
http.get(url)
end

# Performs the search query and returns the resulting SearchResponse
def response(http: HTTParty, logger: Rails.logger)
response = parse_response get(http: http)
logger.debug(response)
LookupResponse.new(response).item
end

private

def parse_response(response)
Hash.from_xml(response.body)
end
attr_reader :asin, :aws_credentials

# Generate the signature required by the Product Advertising API
def signature
Base64.encode64(digest_with_key(string_to_sign)).strip
def process_response(response_hash)
LookupResponse.new(response_hash).item
end

# Generate the string to be signed
def string_to_sign
"GET\n#{ENDPOINT}\n#{REQUEST_URI}\n#{canonical_query_string}"
end

# Generate the canonical query
def canonical_query_string
params.sort
.map { |key, value| "#{uri_escape(key)}=#{uri_escape(value)}" }
.join('&')
end

def params
params = {
'Service' => 'AWSECommerceService',
'AWSAccessKeyId' => aws_credentials.access_key,
'AssociateTag' => aws_credentials.associate_tag,
# endpoint-specific
# Other request parameters for ItemLookup can be found here:
#
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\
# ItemLookup.html#ItemLookup-rp
def request_params
{
'Operation' => 'ItemLookup',
'ResponseGroup' => 'ItemAttributes,Offers,Images',
'ItemId' => asin.to_s
}

# Set current timestamp if not set
params['Timestamp'] ||= Time.now.gmtime.iso8601
params
end

def digest_with_key(string)
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
aws_credentials.secret_key,
string)
end

def uri_escape(phrase)
CGI.escape(phrase.to_s)
end
end
end
Loading