Skip to content

Commit 72a7f8f

Browse files
committed
Extract endpoint superclass
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?
1 parent fa6c620 commit 72a7f8f

File tree

3 files changed

+142
-162
lines changed

3 files changed

+142
-162
lines changed

lib/amazon_product_api/endpoint.rb

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
module AmazonProductAPI
2+
# Base representation of all Amazon Product Advertising API endpoints.
3+
#
4+
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\
5+
# CHAP_OperationListAlphabetical.html
6+
#
7+
# Any general logic relating to lookup, building the query string,
8+
# authentication signatures, etc. should live in this class. Specializations
9+
# (including specific request parameters and response parsing) should live in
10+
# endpoint subclasses.
11+
class Endpoint
12+
require 'httparty'
13+
require 'time'
14+
require 'uri'
15+
require 'openssl'
16+
require 'base64'
17+
18+
# The region you are interested in
19+
ENDPOINT = 'webservices.amazon.com'.freeze
20+
REQUEST_URI = '/onca/xml'.freeze
21+
22+
# Generates the signed URL
23+
def url
24+
raise InvalidQueryError, 'Missing AWS credentials' unless aws_credentials
25+
26+
"http://#{ENDPOINT}#{REQUEST_URI}" + # base
27+
"?#{canonical_query_string}" + # query
28+
"&Signature=#{uri_escape(signature)}" # signature
29+
end
30+
31+
# Sends the HTTP request
32+
def get(http: HTTParty)
33+
http.get(url)
34+
end
35+
36+
# Performs the search query and returns the processed response
37+
def response(http: HTTParty, logger: Rails.logger)
38+
response = parse_response get(http: http)
39+
logger.debug response
40+
process_response(response)
41+
end
42+
43+
private
44+
45+
attr_reader :aws_credentials
46+
47+
# Takes the response hash and returns the processed API response
48+
#
49+
# This must be implemented for each individual endpoint.
50+
def process_response(_response_hash)
51+
raise NotImplementedError, 'Implement this method in your subclass.'
52+
end
53+
54+
# Returns a hash of request parameters unique to the endpoint
55+
#
56+
# This must be implemented for each individual endpoint.
57+
def request_params
58+
raise NotImplementedError, 'Implement this method in your subclass.'
59+
end
60+
61+
def params
62+
params = request_params.merge(
63+
'Service' => 'AWSECommerceService',
64+
'AWSAccessKeyId' => aws_credentials.access_key,
65+
'AssociateTag' => aws_credentials.associate_tag,
66+
)
67+
68+
# Set current timestamp if not set
69+
params['Timestamp'] ||= Time.now.gmtime.iso8601
70+
params
71+
end
72+
73+
def parse_response(response)
74+
Hash.from_xml(response.body)
75+
end
76+
77+
# Generates the signature required by the Product Advertising API
78+
def signature
79+
Base64.encode64(digest_with_key(string_to_sign)).strip
80+
end
81+
82+
def string_to_sign
83+
"GET\n#{ENDPOINT}\n#{REQUEST_URI}\n#{canonical_query_string}"
84+
end
85+
86+
def canonical_query_string
87+
params.sort
88+
.map { |key, value| "#{uri_escape(key)}=#{uri_escape(value)}" }
89+
.join('&')
90+
end
91+
92+
def digest_with_key(string)
93+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
94+
aws_credentials.secret_key,
95+
string)
96+
end
97+
98+
def uri_escape(phrase)
99+
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
100+
end
101+
end
102+
end
+17-78
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,37 @@
1-
require "amazon_product_api/lookup_response"
1+
require 'amazon_product_api/endpoint'
2+
require 'amazon_product_api/lookup_response'
23

34
module AmazonProductAPI
45
# Responsible for looking up an item listing on Amazon
56
#
67
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html
78
#
8-
# Any logic relating to lookup, building the query string, authentication
9-
# signatures, etc. should live in this class.
10-
class ItemLookupEndpoint
11-
require "httparty"
12-
require "time"
13-
require "uri"
14-
require "openssl"
15-
require "base64"
16-
17-
# The region you are interested in
18-
ENDPOINT = "webservices.amazon.com"
19-
REQUEST_URI = "/onca/xml"
20-
21-
attr_accessor :asin, :aws_credentials
22-
9+
# Contains all specialization logic for this endpoint including request
10+
# parameters, parameter validation, and response parsing.
11+
class ItemLookupEndpoint < Endpoint
2312
def initialize(asin, aws_credentials)
2413
@asin = asin
2514
@aws_credentials = aws_credentials
2615
end
2716

28-
# Generate the signed URL
29-
def url
30-
"http://#{ENDPOINT}#{REQUEST_URI}" + # base
31-
"?#{canonical_query_string}" + # query
32-
"&Signature=#{uri_escape(signature)}" # signature
33-
end
34-
35-
# Send the HTTP request
36-
def get(http: HTTParty)
37-
http.get(url)
38-
end
39-
40-
# Performs the search query and returns the resulting SearchResponse
41-
def response(http: HTTParty, logger: Rails.logger)
42-
response = parse_response get(http: http)
43-
logger.debug(response)
44-
LookupResponse.new(response).item
45-
end
46-
47-
4817
private
4918

19+
attr_reader :asin, :aws_credentials
5020

51-
def parse_response(response)
52-
Hash.from_xml(response.body)
21+
def process_response(response_hash)
22+
LookupResponse.new(response_hash).item
5323
end
5424

55-
# Generate the signature required by the Product Advertising API
56-
def signature
57-
Base64.encode64(digest_with_key string_to_sign).strip
58-
end
59-
60-
# Generate the string to be signed
61-
def string_to_sign
62-
"GET\n#{ENDPOINT}\n#{REQUEST_URI}\n#{canonical_query_string}"
63-
end
64-
65-
# Generate the canonical query
66-
def canonical_query_string
67-
params.sort
68-
.map { |key, value| "#{uri_escape(key)}=#{uri_escape(value)}" }
69-
.join("&")
70-
end
71-
72-
def params
73-
params = {
74-
"Service" => "AWSECommerceService",
75-
"AWSAccessKeyId" => aws_credentials.access_key,
76-
"AssociateTag" => aws_credentials.associate_tag,
77-
# endpoint-specific
78-
"Operation" => "ItemLookup",
79-
"ResponseGroup" => "ItemAttributes,Offers,Images",
80-
"ItemId" => asin.to_s,
25+
# Other request parameters for ItemLookup can be found here:
26+
#
27+
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\
28+
# ItemLookup.html#ItemLookup-rp
29+
def request_params
30+
{
31+
'Operation' => 'ItemLookup',
32+
'ResponseGroup' => 'ItemAttributes,Offers,Images',
33+
'ItemId' => asin.to_s,
8134
}
82-
83-
# Set current timestamp if not set
84-
params["Timestamp"] ||= Time.now.gmtime.iso8601
85-
params
86-
end
87-
88-
def digest_with_key(string)
89-
OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"),
90-
aws_credentials.secret_key,
91-
string)
92-
end
93-
94-
def uri_escape(phrase)
95-
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
9635
end
9736
end
9837
end
+23-84
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,42 @@
1-
require "amazon_product_api/search_response"
1+
require 'amazon_product_api/endpoint'
2+
require 'amazon_product_api/search_response'
23

34
module AmazonProductAPI
45
# Responsible for building and executing an Amazon Product API search query.
56
#
67
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemSearch.html
78
#
8-
# Any logic relating to searching, building the query string, authentication
9-
# signatures, etc. should live in this class.
10-
class ItemSearchEndpoint
11-
require "httparty"
12-
require "time"
13-
require "uri"
14-
require "openssl"
15-
require "base64"
16-
17-
# The region you are interested in
18-
ENDPOINT = "webservices.amazon.com"
19-
REQUEST_URI = "/onca/xml"
20-
21-
attr_accessor :query, :page, :aws_credentials
22-
9+
# Contains all specialization logic for this endpoint including request
10+
# parameters, parameter validation, and response parsing.
11+
class ItemSearchEndpoint < Endpoint
2312
def initialize(query, page, aws_credentials)
13+
raise InvalidQueryError, "Page can't be nil." if page.nil?
14+
2415
@query = query
2516
@page = page
2617
@aws_credentials = aws_credentials
2718
end
2819

29-
# Generate the signed URL
30-
def url
31-
raise InvalidQueryError unless query && page
32-
33-
"http://#{ENDPOINT}#{REQUEST_URI}" + # base
34-
"?#{canonical_query_string}" + # query
35-
"&Signature=#{uri_escape(signature)}" # signature
36-
end
37-
38-
# Send the HTTP request
39-
def get(http: HTTParty)
40-
http.get(url)
41-
end
42-
43-
# Performs the search query and returns the resulting SearchResponse
44-
def response(http: HTTParty, logger: Rails.logger)
45-
response = parse_response get(http: http)
46-
logger.debug response
47-
SearchResponse.new response
48-
end
49-
50-
5120
private
5221

22+
attr_accessor :query, :page, :aws_credentials
5323

54-
def parse_response(response)
55-
Hash.from_xml(response.body)
56-
end
57-
58-
# Generate the signature required by the Product Advertising API
59-
def signature
60-
Base64.encode64(digest_with_key string_to_sign).strip
61-
end
62-
63-
# Generate the string to be signed
64-
def string_to_sign
65-
"GET\n#{ENDPOINT}\n#{REQUEST_URI}\n#{canonical_query_string}"
66-
end
67-
68-
# Generate the canonical query
69-
def canonical_query_string
70-
params.sort
71-
.map { |key, value| "#{uri_escape(key)}=#{uri_escape(value)}" }
72-
.join("&")
73-
end
74-
75-
def params
76-
params = {
77-
"Service" => "AWSECommerceService",
78-
"AWSAccessKeyId" => aws_credentials.access_key,
79-
"AssociateTag" => aws_credentials.associate_tag,
80-
# endpoint-specific
81-
"Operation" => "ItemSearch",
82-
"ResponseGroup" => "ItemAttributes,Offers,Images",
83-
"SearchIndex" => "All",
84-
"Keywords" => query.to_s,
85-
"ItemPage" => page.to_s
24+
def process_response(response_hash)
25+
SearchResponse.new response_hash
26+
end
27+
28+
# Other request parameters for ItemLookup can be found here:
29+
#
30+
# http://docs.aws.amazon.com/AWSECommerceService/latest/DG/\
31+
# ItemSearch.html#ItemSearch-rp
32+
def request_params
33+
{
34+
'Operation' => 'ItemSearch',
35+
'ResponseGroup' => 'ItemAttributes,Offers,Images',
36+
'SearchIndex' => 'All',
37+
'Keywords' => query.to_s,
38+
'ItemPage' => page.to_s,
8639
}
87-
88-
# Set current timestamp if not set
89-
params["Timestamp"] ||= Time.now.gmtime.iso8601
90-
params
91-
end
92-
93-
def digest_with_key(string)
94-
OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"),
95-
aws_credentials.secret_key,
96-
string)
97-
end
98-
99-
def uri_escape(phrase)
100-
URI.escape(phrase.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
10140
end
10241
end
10342
end

0 commit comments

Comments
 (0)