-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Amazon Product API "ItemLookup" endpoint (#173)
* Refactor AmazonProductAPI In preparation for #62, this commit pulls the endpoint-specific information out of the HTTPClient class and into and endpoint-specific class. Now HTTPClient is responsible only for managing the environment information and directing the user to the relevant endpoint. Adding a new endpoint is as simple as adding a method (ex. `item_search`) and corresponding class (ex. `ItemSearchEndpoint). The interface is *not* in its final form yet. This is just a good breaking point for a commit. * Add ItemSearch endpoint Building on the last commit, this adds a new ItemLookup endpoint. Now we can pull details on an individual item based on the ASIN. The endpoints and responses include a lot of duplication. Some refactoring will probably be needed. * Extract spec for ItemSearchEndpoint When the item search endpoint was extracted, the specs weren't extracted with it; this means that the HTTPClientSpec was testing everything relating to the item search endpoint. This commit extracts all specs relating to the endpoint into a new file. This can now be refactored and copied over for the `ItemLookupEndpointSpec`. * Add spec for ItemLookupEndpoint * Fix CodeClimate issue This is a bit of a hack solution, but I don't feel comfortable doing any major abstraction here yet; I don't think we have enough information. Hopefully this'll clear up the CodeClimate complaints about duplicated code! * Fix easy PR-related rubocop issues Two big points: 1. This only fixes easy rubocop issues related to this PR. This doesn't touch any of the new capistrano code; I want the diff for this PR to be fairly contained. 2. This includes two rubocop config changes: i. Exclude the vendor directory from linting ii. Allow multiline braces in tests (for exception checks, let, etc.) There are a few more risky/controversial rubocop changes I omitted from this commit. Those are coming next. * Fix all Lint/UriEscapeUnescape violations Fixes #56 This commit changes all the `URI.escape` calls to `CGI.escape`. All tests pass and the app still works. As a bonus, this seems to resolve issue #56 too-apostrophes are now properly escaped. This resolves all rubocop issues relating to this PR. * Give better name to AWS test credentials in specs Following the review suggestions. This renames `env` to `aws_credentials`. The latter is a better name because it represents a credentials object (built from env vars), not the ENV object itself. Hopefully this will be clearer! * Fix item lookup hash When I fixed the code style for Code Climate in #173, I used the wrong hash leading to no update attributes being found. This fixes the bug, adds a test, and renames some of the variables.
- Loading branch information
Showing
14 changed files
with
416 additions
and
142 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# frozen_string_literal: true | ||
|
||
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 | ||
|
||
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 | ||
|
||
# Generate the signature required by the Product Advertising API | ||
def signature | ||
Base64.encode64(digest_with_key(string_to_sign)).strip | ||
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 | ||
'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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'amazon_product_api/search_response' | ||
|
||
module AmazonProductAPI | ||
# Responsible for building and executing an Amazon Product API search query. | ||
# | ||
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemSearch.html | ||
# | ||
# Any logic relating to searching, building the query string, authentication | ||
# signatures, etc. should live in this class. | ||
class ItemSearchEndpoint | ||
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 :query, :page, :aws_credentials | ||
|
||
def initialize(query, page, aws_credentials) | ||
@query = query | ||
@page = page | ||
@aws_credentials = aws_credentials | ||
end | ||
|
||
# Generate the signed URL | ||
def url | ||
raise InvalidQueryError unless query && page | ||
|
||
"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 | ||
SearchResponse.new response | ||
end | ||
|
||
private | ||
|
||
def parse_response(response) | ||
Hash.from_xml(response.body) | ||
end | ||
|
||
# Generate the signature required by the Product Advertising API | ||
def signature | ||
Base64.encode64(digest_with_key(string_to_sign)).strip | ||
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 | ||
'Operation' => 'ItemSearch', | ||
'ResponseGroup' => 'ItemAttributes,Offers,Images', | ||
'SearchIndex' => 'All', | ||
'Keywords' => query.to_s, | ||
'ItemPage' => page.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 |
Oops, something went wrong.