-
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.
I'm pretty torn on this commit-an abstract superclass doesn't seem like a very ruby thing to do. Maybe I've been doing too much Java? There are two main goals of this commit: 1. It should be trivial to add new endpoints. We're going to have to add a few more, and being able to customize only the specialized bits will save a ton of time and prevent bugs. 2. Shared code should exist in one location. Already, we've got a bug in our signing process, and keeping this all in a superclass means we only need to fix it in one place. I don't like how I'm handling `@aws_credentials` at the moment (requiring each subclass to initialize it); any ideas of a better way to handle it?
- Loading branch information
Showing
3 changed files
with
142 additions
and
162 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
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'.freeze | ||
REQUEST_URI = '/onca/xml'.freeze | ||
|
||
# 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) | ||
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) | ||
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 |
---|---|---|
@@ -1,98 +1,37 @@ | ||
require "amazon_product_api/lookup_response" | ||
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 | ||
|
||
attr_reader :asin, :aws_credentials | ||
|
||
def parse_response(response) | ||
Hash.from_xml(response.body) | ||
def process_response(response_hash) | ||
LookupResponse.new(response_hash).item | ||
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, | ||
# 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) | ||
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) | ||
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 |
---|---|---|
@@ -1,103 +1,42 @@ | ||
require "amazon_product_api/search_response" | ||
require 'amazon_product_api/endpoint' | ||
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 | ||
|
||
# Contains all specialization logic for this endpoint including request | ||
# parameters, parameter validation, and response parsing. | ||
class ItemSearchEndpoint < Endpoint | ||
def initialize(query, page, aws_credentials) | ||
raise InvalidQueryError, "Page can't be nil." if page.nil? | ||
|
||
@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 | ||
|
||
attr_accessor :query, :page, :aws_credentials | ||
|
||
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 | ||
def process_response(response_hash) | ||
SearchResponse.new response_hash | ||
end | ||
|
||
# Other request parameters for ItemLookup can be found here: | ||
# | ||
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\ | ||
# ItemSearch.html#ItemSearch-rp | ||
def request_params | ||
{ | ||
'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) | ||
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) | ||
end | ||
end | ||
end |