amazon-product-advertising-api-prezjordan 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ ._*
3
+ rdoc
4
+ test/test_local.rb
@@ -0,0 +1,128 @@
1
+ Basic usage
2
+ ###########
3
+
4
+ require 'rubygems'
5
+ require 'amazon_product_advertising_api'
6
+
7
+ # Setup your API keys - in an initializer or something like that
8
+ AmazonProductAdvertisingApi::Base.access_key_id = "<insert api key here>"
9
+ AmazonProductAdvertisingApi::Base.secret_access_key = "<insert secret access key here>"
10
+
11
+ # Setup Associates IDs for whichever regions you're selling to
12
+ AmazonProductAdvertisingApi::Base.associate_ids.uk = "<insert UK Associate ID here>"
13
+ AmazonProductAdvertisingApi::Base.associate_ids.us = "<insert US Associate ID here>"
14
+
15
+ lookup = AmazonProductAdvertisingApi::Operations::Item::ItemLookup.new("0201485419")
16
+ lookup.run
17
+
18
+ # Returned XML, tidied
19
+ # <?xml version="1.0"?>
20
+ # <ItemLookupResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05">
21
+ # <OperationRequest>
22
+ # <RequestId>d6c19392-7710-4b05-ad9c-1637755170d8</RequestId>
23
+ # <Arguments>
24
+ # <Argument Name="Operation" Value="ItemLookup"/>
25
+ # <Argument Name="Service" Value="AWSECommerceService"/>
26
+ # <Argument Name="ItemId" Value="0201485419"/>
27
+ # <Argument Name="AWSAccessKeyId" Value="<your api key>"/>
28
+ # </Arguments>
29
+ # <RequestProcessingTime>0.0376290000000000</RequestProcessingTime>
30
+ # </OperationRequest>
31
+ # <Items>
32
+ # <Request>
33
+ # <IsValid>True</IsValid>
34
+ # <ItemLookupRequest>
35
+ # <Condition>New</Condition>
36
+ # <DeliveryMethod>Ship</DeliveryMethod>
37
+ # <IdType>ASIN</IdType>
38
+ # <MerchantId>Amazon</MerchantId>
39
+ # <OfferPage>1</OfferPage>
40
+ # <ItemId>0201485419</ItemId>
41
+ # <ResponseGroup>Small</ResponseGroup>
42
+ # <ReviewPage>1</ReviewPage>
43
+ # </ItemLookupRequest>
44
+ # </Request>
45
+ # <Item>
46
+ # <ASIN>0201485419</ASIN>
47
+ # <DetailPageURL>http://www.amazon.co.uk/Art-Computer-Programming-Information-Processing/dp/0201485419%3FSubscriptionId%3D<your api key>%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0201485419</DetailPageURL>
48
+ # <ItemAttributes>git
49
+ # <Author>Donald E. Knuth</Author>
50
+ # <Manufacturer>Addison Wesley</Manufacturer>
51
+ # <ProductGroup>Book</ProductGroup>
52
+ # <Title>The Art of Computer Programming: v. 1-3: Vol 1-3 (Series in Computer Science &amp; Information Processing)</Title>
53
+ # </ItemAttributes>
54
+ # </Item>
55
+ # </Items>
56
+ # </ItemLookupResponse>
57
+
58
+ # is_valid is part of the api and says whether the request was valid
59
+ lookup.is_valid
60
+ => true
61
+
62
+ lookup.response.items.size
63
+ => 1
64
+
65
+ lookup.response.items.first
66
+ => #<AmazonProductAdvertisingApi::Item:0x139154c @detail_page_url="http://www.amazon.co.uk/Art-Computer-Programming-Information-Processing/dp/0201485419%3FSubscriptionId%3D<your api key>%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0201485419", @item_attributes=#<AmazonProductAdvertisingApi::Container:0x1391448 @manufacturer="Addison Wesley", @product_group="Book", @title="The Art of Computer Programming: v. 1-3: Vol 1-3 (Series in Computer Science &amp; Information Processing)", @author="Donald E. Knuth">, @asin="0201485419">
67
+
68
+ lookup.response.items.first.asin
69
+ => "0201485419"
70
+
71
+ # ItemAttributes is a container element, doesn't return a string
72
+ lookup.response.items.first.item_attributes
73
+ => #<AmazonProductAdvertisingApi::Container:0x1391448 @manufacturer="Addison Wesley", @product_group="Book", @title="The Art of Computer Programming: v. 1-3: Vol 1-3 (Series in Computer Science &amp; Information Processing)", @author="Donald E. Knuth">
74
+
75
+ lookup.response.items.first.item_attributes.title
76
+ => "The Art of Computer Programming: v. 1-3: Vol 1-3 (Series in Computer Science &amp; Information Processing)"
77
+
78
+ lookup.response.items.first.item_attributes.author
79
+ => "Donald E. Knuth"
80
+
81
+ # If you need to debug, you can access all the xml data easily
82
+ lookup.raw_data
83
+ => <XML string, straight from the request body>
84
+
85
+ lookup.hpricot_data
86
+ => <the xml data, having been fed into Hpricot>
87
+
88
+ # And also the request uri
89
+ lookup.request_uri
90
+ => #<URI::HTTP:0x1c7021c URL:http://ecs.amazonaws.co.uk/onca/xml?Service=AWSECommerceService&Operation=ItemLookup&ItemId=0201485419&AWSAccessKeyId=%3Cinsert%20api%20key%20here%3E>
91
+
92
+
93
+ Error handling - invalid request credentials, url form, etc
94
+ ###########################################################
95
+ AmazonProductAdvertisingApi::Base.access_key_id = "<an invalid id>"
96
+ lookup = AmazonProductAdvertisingApi::Operations::Item::ItemLookup.new("0201485419")
97
+ lookup.run
98
+ => false
99
+
100
+ lookup.is_valid
101
+ => false
102
+
103
+ lookup.errors
104
+ => <an array of error objects>
105
+
106
+ lookup.errors.first.code
107
+ => AWS.InvalidParameterValue
108
+
109
+ lookup.errors.first.message
110
+ => <an invalid id> is not a valid value for AWSAccessKeyId. Please change this value and retry your request.
111
+
112
+
113
+ Error handling - invalid request credentials, url form, etc
114
+ ###########################################################
115
+ AmazonProductAdvertisingApi::Base.access_key_id = "<valid api key>"
116
+ lookup = AmazonProductAdvertisingApi::Operations::Item::ItemLookup.new("<invalid asin>")
117
+ lookup.run
118
+ => false
119
+
120
+ # is_valid returns true as the request was well formed, it just doesn't have any valid data to return
121
+ lookup.is_valid
122
+ => true
123
+
124
+ lookup.errors.first.code
125
+ => AWS.InvalidParameterValue
126
+
127
+ lookup.errors.first.message
128
+ => <an invalid asin> is not a valid value for ItemId. Please change this value and retry your request.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Jon Gilbraith, CompletelyNovel Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,99 @@
1
+ = amazon-product-advertising-api
2
+
3
+ == Introduction
4
+ A nice rubyish interface to the Amazon Product Advertising API, formerly
5
+ known as the Associates Web Service and before that the Amazon E-Commerce
6
+ Service.
7
+
8
+ The basic design philosophy is the 'Principle of least surprise', so it's:
9
+ - a dsl that should feel familiar to ruby folk.
10
+ - structured generally like the API.
11
+
12
+ == Prerequisites
13
+ It's assumed you're reasonably familiar with Ruby as a language, and the
14
+ typical coding patterns and styles employed by the community.
15
+
16
+ It's also assumed that you're familiar with the API. If not go RTFM for a bit,
17
+ it's all pretty straight forward.
18
+
19
+ == Overview
20
+ As mentioned before, the idea behind this is to model the API so that once
21
+ you're familiar with this library and the API, you'll find this an easy way to
22
+ navigate around it. You should then be able to just refer to the lists of
23
+ Operations, Response Groups and Response Elements in the appendices for all
24
+ you need to know.
25
+
26
+ You initiate requests by instantiating classes representing api Operations and
27
+ named the same way, i.e. ItemSearch, ItemLookup, etc. The request parameters
28
+ are represented by attributes on those objects and can be manipulated as you
29
+ wish. If a parameter is required it'll be required or defaulted on
30
+ initialization, but everywhere else we defer to the defaults provided by the
31
+ api.
32
+
33
+ Once setup, you call 'run' on the operation which will send the request and
34
+ return a response object (and after that also available as operation.response).
35
+
36
+ The response has a bit less structure than the request operations. There is no
37
+ representation for Response Groups as beyond one level deep there is no clear
38
+ pattern for how returned XML is structured (at least not without going down the
39
+ route trying to define custom handling for each of them). So, instead we group
40
+ the Operations into broad groups which return the same collections of data.
41
+ ItemSearch, ItemLookup and SimilarityLookup all return a Items container
42
+ element with one or more Item tag in it, etc.
43
+
44
+ Everything beyond one level deep is done with a recursive sweep instantiating
45
+ Element objects as we go.
46
+
47
+ == Things to know
48
+ - Amazon uses a lot of Camel case in their XML, but to suit Ruby / Rails
49
+ conventions attributes, etc are converted to underscore style.
50
+ - One gotcha - Response Elements can either be a simple value or a container
51
+ with more elements inside. We recognise this situation by simply comparing
52
+ the parent and child names (plural parent, singular child) though that isn't
53
+ always the pattern so we also look for multiple children of the same name.
54
+
55
+ That means that some situations might creep through and not work as expected.
56
+ If the parent and child don't follow the same naming convention and there is
57
+ also only one child element you'd find the child would be defined as
58
+ parent.child rather than parent.children.first.
59
+
60
+ This is a bug and will have to be addressed (probably by making a list of
61
+ container elements to check against).
62
+
63
+ == Having problems?
64
+ After running the request uri, returned raw xml and hpricot data will be
65
+ available in operation.raw_data and operation.hpricot_data which should tell
66
+ you all you need to know about the response.
67
+
68
+ == Test coverage
69
+ There aren't any tests at the moment but I'll try to add some.
70
+
71
+ For the most part there isn't really all that much to test though as it's all
72
+ largely just making http requests, however the XML parsing is quite convoluted
73
+ so some tests would be good there to make sure the parsing is following the
74
+ intended rules.
75
+
76
+
77
+ = TODO
78
+ - Implement the rest of the Operations (Cart*, Customer*, Help, List*, Seller*,
79
+ Tag*, Transaction* and Vehicle*).
80
+ - Implement batch and multiple operation requests, abstracted away from the
81
+ user within the dsl.
82
+ - Some sort of internal caching mechanism.
83
+ - Ability to bind to different IP addresses, in order to circumvent API rate
84
+ limiting.
85
+
86
+
87
+ = Obtaining
88
+ The main repository is at github but there is also a rubyforge project.
89
+
90
+ - https://github.com/completelynovel/amazon-product-advertising-api/tree
91
+ - http://amazon-pa-api.rubyforge.org/
92
+
93
+
94
+ = Credits
95
+ Created by Jon Gilbraith, jon@completelynovel.com, while working with
96
+ CompletelyNovel.
97
+
98
+ Contents of support.rb are basically taken and adapted from Rails'
99
+ ActiveSupport.
@@ -0,0 +1,29 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Generate documentation for the amazon-product-advertising-api gem.'
6
+ Rake::RDocTask.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = 'rdoc'
8
+ rdoc.title = 'AmazonProductAdvertisingApi'
9
+ rdoc.options << '--line-numbers' << '--inline-source'
10
+ rdoc.rdoc_files.include('README.rdoc')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
13
+
14
+ begin
15
+ require 'jeweler'
16
+ Jeweler::Tasks.new do |gemspec|
17
+ gemspec.name = "amazon-product-advertising-api"
18
+ gemspec.summary = "A nice rubyish interface to the Amazon Product Advertising API."
19
+ gemspec.email = "jon@completelynovel.com"
20
+ gemspec.homepage = "http://github.com/completelynovel/amazon-product-advertising-api"
21
+ gemspec.description = "A nice rubyish interface to the Amazon Product Advertising API, formerly known as the Associates Web Service and before that the Amazon E-Commerce Service."
22
+ gemspec.authors = ["Jon Gilbraith"]
23
+ gemspec.add_dependency("hpricot")
24
+ gemspec.add_dependency("ruby-hmac")
25
+ end
26
+ rescue LoadError
27
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
28
+ end
29
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1,53 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{amazon-product-advertising-api-prezjordan}
5
+ s.version = "0.2.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jon Gilbraith", "Jordan Scales"]
9
+ s.date = %q{2009-07-30}
10
+ s.description = %q{A nice rubyish interface to the Amazon Product Advertising API (MODIFIED to handle ResponseGroup), formerly known as the Associates Web Service and before that the Amazon E-Commerce Service.}
11
+ s.email = %q{jon@completelynovel.com}
12
+ s.extra_rdoc_files = [
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "EXAMPLE.txt",
18
+ "MIT-LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "amazon-product-advertising-api.gemspec",
23
+ "lib/amazon_product_advertising_api.rb",
24
+ "lib/amazon_product_advertising_api/base.rb",
25
+ "lib/amazon_product_advertising_api/operations/base.rb",
26
+ "lib/amazon_product_advertising_api/operations/browse_node.rb",
27
+ "lib/amazon_product_advertising_api/operations/item.rb",
28
+ "lib/amazon_product_advertising_api/response_elements.rb",
29
+ "lib/amazon_product_advertising_api/support.rb"
30
+ ]
31
+ s.has_rdoc = true
32
+ s.homepage = %q{http://github.com/prezjordan/amazon-product-advertising-api}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.1}
36
+ s.summary = %q{A nice rubyish interface to the Amazon Product Advertising API. (MODIFIED to handle ResponseGroup)}
37
+
38
+ if s.respond_to? :specification_version then
39
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
+ s.specification_version = 2
41
+
42
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
43
+ s.add_runtime_dependency(%q<hpricot>, [">= 0"])
44
+ s.add_runtime_dependency(%q<ruby-hmac>, [">= 0"])
45
+ else
46
+ s.add_dependency(%q<hpricot>, [">= 0"])
47
+ s.add_dependency(%q<ruby-hmac>, [">= 0"])
48
+ end
49
+ else
50
+ s.add_dependency(%q<hpricot>, [">= 0"])
51
+ s.add_dependency(%q<ruby-hmac>, [">= 0"])
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ require 'net/http'
2
+ require 'cgi'
3
+ require 'hpricot'
4
+ require 'time'
5
+ require 'hmac'
6
+ require 'hmac-sha2'
7
+ require 'base64'
8
+
9
+ require 'amazon_product_advertising_api/support'
10
+
11
+ Class.send(:include, AmazonProductAdvertisingApi::CoreExtensions::Class)
12
+ String.send(:include, AmazonProductAdvertisingApi::CoreExtensions::String)
13
+
14
+ require 'amazon_product_advertising_api/base'
15
+ require 'amazon_product_advertising_api/response_elements'
16
+ require 'amazon_product_advertising_api/operations/base'
17
+ require 'amazon_product_advertising_api/operations/browse_node'
18
+ require 'amazon_product_advertising_api/operations/item'
@@ -0,0 +1,24 @@
1
+ module AmazonProductAdvertisingApi #:nodoc:
2
+
3
+ # This is the main base class where you define your config data. Setup like so:
4
+ # AmazonProductAdvertisingApi.base.access_key_id = <your Amazon AccessKeyId>
5
+ # AmazonProductAdvertisingApi.base.secret_access_key = <your Amazon SecretAccessKey>
6
+ #
7
+ # You also setup your Associates codes for different regions here (the right one is supplied)
8
+ # based on what region you are requesting for.
9
+ # AmazonProductAdvertisingApi.base.associate_ids.ca = <your associates CA Affiliate key>
10
+ # AmazonProductAdvertisingApi.base.associate_ids.uk = <your associates UK Affiliate key>
11
+ class Base
12
+
13
+ cattr_accessor :access_key_id
14
+
15
+ cattr_accessor :secret_access_key
16
+
17
+ cattr_accessor :response_group
18
+
19
+ cattr_accessor :associate_ids
20
+ @@associate_ids = Struct.new(:ca, :de, :fr, :jp, :uk, :us).new
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,184 @@
1
+ module AmazonProductAdvertisingApi #:nodoc:
2
+ module Operations #:nodoc:
3
+ module Base #:nodoc:
4
+
5
+ # This is the parent class of any Operations performed.
6
+ #
7
+ # Each class should have a constant defined called REQUEST_PARAMETERS which contains the available parameters
8
+ # as defined in the API docs. Each class should also override the parse method with a custom version to suit the
9
+ # particular pattern of XML returned for it.
10
+ #
11
+ # Subclasses should also override initialize to take any parameters the API defines as required, and follow the pattern
12
+ # of the region being the last parameter.
13
+ #
14
+ # Before doing that though it should call super back to this one.
15
+ class Request
16
+
17
+ # String - Amazon API calls can be sent to any of 6 regions, so this defines which one.
18
+ # It'll also use this data to pick the right Associates key to use.
19
+ attr_accessor :region
20
+
21
+ # String - The name of the Operation you want to perform, i.e. ItemSearch, ItemLookup, etc.
22
+ attr_accessor :operation
23
+
24
+ # String - This stores the request that gets sent to Amazon (for investigation if you want to look under the covers).
25
+ attr_accessor :request_uri
26
+
27
+ # String - This stores the raw data of the request response (again, for investigation if you want to look under the covers).
28
+ attr_accessor :raw_data
29
+
30
+ # Hpricot - This stores the raw data of the request response should you feel the need / desire to do some parsing yourself.
31
+ attr_accessor :hpricot_data
32
+
33
+ # Element - This is the root of the structure that the lib assembles from the data when parsed.
34
+ attr_accessor :response
35
+
36
+ # Boolean - All responses have a field saying whether the request was valid. Note that this refers to the format of the request, etc
37
+ # and not errors with the request parameters, etc. I.e. a lookup for an item that doesn't exsist is still valid.
38
+ attr_accessor :is_valid
39
+
40
+ # Array of Struct - Any errors will be added to this attribute and each has an attribute of code or message.
41
+ attr_accessor :errors
42
+
43
+ SERVICE_URLS = {
44
+ :us => 'http://ecs.amazonaws.com/onca/xml',
45
+ :uk => 'http://ecs.amazonaws.co.uk/onca/xml',
46
+ :ca => 'http://ecs.amazonaws.ca/onca/xml',
47
+ :de => 'http://ecs.amazonaws.de/onca/xml',
48
+ :jp => 'http://webservices.amazon.co.jp/onca/xml',
49
+ :fr => 'http://ecs.amazonaws.fr/onca/xml'
50
+ }
51
+
52
+ API_VERSION = "2009-03-31"
53
+
54
+ # hacky :(
55
+ def initialize(arg1, arg2)
56
+ self.response = AmazonProductAdvertisingApi::Operations::Base::Element.new
57
+ self.errors = []
58
+ end
59
+
60
+ # This takes care of building request, performing it, storing the results, checking for errors then parsing the data (if the request was valid).
61
+ def query_amazon(params)
62
+ request_params = {}
63
+ request_params["Service"] = "AWSECommerceService"
64
+ request_params["SignatureVersion"] = 2
65
+ request_params["SignatureMethod"] = "HmacSHA256"
66
+ request_params["Timestamp"] = Time.now.gmtime.iso8601
67
+ request_params["AWSAccessKeyId"] = AmazonProductAdvertisingApi::Base.access_key_id
68
+ request_params["Operation"] = self.operation
69
+ request_params["AssociateTag"] = AmazonProductAdvertisingApi::Base.associate_ids.send(self.region) unless AmazonProductAdvertisingApi::Base.associate_ids.send(self.region).nil?
70
+ request_params["Version"] = API_VERSION
71
+ request_params["ResponseGroup"] = AmazonProductAdvertisingApi::Base.response_group.try(:join, ',')
72
+
73
+ request_params.merge!(params)
74
+
75
+ # Process all params - make sure they're all strings, camelize and escape (where appropriate)
76
+ request_params = request_params.collect { |var, val| [var.to_s.camelize, val.to_s] }
77
+ request_params = request_params.collect { |var, val| [var, CGI::escape(val).gsub('+', '%20')] }
78
+
79
+ # Assemble into a full request string
80
+ unsigned_uri = URI.parse("#{SERVICE_URLS[self.region]}?#{request_params.sort { |a, b| a[0] <=> b[0] }.collect { |var, val| var + "=" + val }.join("&")}")
81
+
82
+ # Generate hmac
83
+ hmac = HMAC::SHA256.new(AmazonProductAdvertisingApi::Base.secret_access_key)
84
+ hmac.update("GET\n#{unsigned_uri.host}\n#{unsigned_uri.path}\n#{unsigned_uri.query}")
85
+
86
+ self.request_uri = URI.parse("#{unsigned_uri}&Signature=#{CGI::escape(Base64.encode64(hmac.digest).chomp)}")
87
+
88
+ result = Net::HTTP::get_response(self.request_uri)
89
+ raise("Error connecting to Amazon - #{result.to_s}") if !result.kind_of?(Net::HTTPSuccess)
90
+
91
+ # Store away the raw data for debugging or if more direct access is required
92
+ self.raw_data = result.body
93
+ self.hpricot_data = Hpricot.XML(self.raw_data)
94
+
95
+ # Now parse the xml and build out the reponse elements
96
+ self.is_valid = self.hpricot_data.at(:IsValid).inner_html == "True"
97
+
98
+ self.parse
99
+
100
+ # is_valid only refers to the request, so we could still have errors - check and parse if present
101
+ if !self.hpricot_data.at(:Errors).nil?
102
+ self.hpricot_data.at(:Errors).search(:Error).each do |error|
103
+ self.errors << Struct.new(:code, :message).new(error.at(:Code).inner_html, error.at(:Message).inner_html)
104
+ end
105
+ end
106
+
107
+ # Return false if it's not a valid request, otherwise return the response
108
+ self.is_valid ? self.response : false
109
+ end
110
+
111
+ # The parse method of a request should be overwritted by any subclasses to account for different patterns in the XML.
112
+ def parse
113
+ raise "This should be being overridden by it's subclass to provide custom parsing for the particular operation concerned."
114
+ end
115
+
116
+ # Launches the request's query to Amazon (via query_amazon).
117
+ def run
118
+ self.query_amazon(params)
119
+ end
120
+
121
+ private
122
+ # When passed an hpricot element it returns true or false based on whether this item is thought to be inside a Container Element.
123
+ # It does this in the rather crude way of seeing if the parent's name is the pluralized form of it's own, or it is one of
124
+ # several with the same name. This isn't 100% fool proof so I think at some point using a definitive list of the container elements
125
+ # would be a better way to go.
126
+ #
127
+ # The pluralisation could also do with something a bit more sophisticated.
128
+ def parent_a_container?(hpricot_element)
129
+ hpricot_element.parent.name == hpricot_element.name + "s" || hpricot_element.parent.search("> #{hpricot_element.name}").size > 1
130
+ end
131
+
132
+ end
133
+
134
+ # XML data that is returned by Amazon gets built into a tree of nodes, which are made up of instances of this class.
135
+ # They represent the 'response element' entity within the API docs.
136
+ #
137
+ # As well as various having attributes it can also contain a collection and behave like an array.
138
+ class Element < Array
139
+
140
+ attr_reader :attributes
141
+
142
+ def initialize
143
+ @attributes = {}
144
+ end
145
+
146
+ # Defines a new accessor on the element and if supplied assigns that attribute a value.
147
+ def add_element(name, value = nil)
148
+ name = name.underscore
149
+
150
+ @attributes[name.to_s] = value
151
+
152
+ self.instance_eval %{
153
+ def self.#{name}
154
+ @#{name}
155
+ end
156
+ def self.#{name}=(value)
157
+ @#{name} ||= value
158
+ end
159
+ }
160
+
161
+ if !value.nil?
162
+ value = value.to_s if value.is_a?(Symbol)
163
+ self.send("#{name}=", value)
164
+ end
165
+
166
+ # Return the element
167
+ self.instance_eval("self.#{name}")
168
+ end
169
+
170
+ def method_missing(method, *args)
171
+ return super unless RESPONSE_ELEMENTS.include?(method.to_sym)
172
+
173
+ if CONTAINER_RESPONSE_ELEMENTS.include?(method.to_sym)
174
+ self.class.new
175
+ else
176
+ nil
177
+ end
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+ end
184
+ end