rasin 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/.document +3 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +13 -0
  4. data/CHANGELOG.rdoc +49 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +54 -0
  7. data/README.rdoc +166 -0
  8. data/asin.gemspec +36 -0
  9. data/lib/asin.rb +8 -0
  10. data/lib/asin/client.rb +398 -0
  11. data/lib/asin/configuration.rb +105 -0
  12. data/lib/asin/simple_cart.rb +54 -0
  13. data/lib/asin/simple_item.rb +44 -0
  14. data/lib/asin/simple_node.rb +44 -0
  15. data/lib/asin/version.rb +3 -0
  16. data/rakefile.rb +11 -0
  17. data/spec/asin.yml +4 -0
  18. data/spec/browse_node_spec.rb +17 -0
  19. data/spec/cart_spec.rb +150 -0
  20. data/spec/cassettes/asin/asin_cart_with_an_existing_cart_should_add_items_to_a_cart.yml +68 -0
  21. data/spec/cassettes/asin/asin_cart_with_an_existing_cart_should_clear_a_cart.yml +66 -0
  22. data/spec/cassettes/asin/asin_cart_with_an_existing_cart_should_get_a_cart.yml +67 -0
  23. data/spec/cassettes/asin/asin_cart_with_an_existing_cart_should_update_a_cart.yml +70 -0
  24. data/spec/cassettes/asin/browse_node_should_lookup_a_browse_node.yml +38 -0
  25. data/spec/cassettes/asin/cart_should_create_a_cart.yml +36 -0
  26. data/spec/cassettes/asin/lookup_and_search_should_have_metadata.yml +81 -0
  27. data/spec/cassettes/asin/lookup_and_search_should_lookup_a_book.yml +81 -0
  28. data/spec/cassettes/asin/lookup_and_search_should_lookup_multiple_books.yml +131 -0
  29. data/spec/cassettes/asin/lookup_and_search_should_lookup_multiple_response_groups.yml +43 -0
  30. data/spec/cassettes/asin/lookup_and_search_should_return_a_custom_item_class.yml +81 -0
  31. data/spec/cassettes/asin/lookup_and_search_should_return_a_mash_value.yml +81 -0
  32. data/spec/cassettes/asin/lookup_and_search_should_return_a_rash_value.yml +81 -0
  33. data/spec/cassettes/asin/lookup_and_search_should_return_a_raw_value.yml +81 -0
  34. data/spec/cassettes/asin/lookup_and_search_should_search_keywords_a_book_with_fulltext.yml +538 -0
  35. data/spec/cassettes/asin/lookup_and_search_should_search_keywords_and_handle_a_single_result.yml +72 -0
  36. data/spec/cassettes/asin/lookup_and_search_should_search_keywords_never_mind_music.yml +119 -0
  37. data/spec/cassettes/asin/lookup_and_search_should_search_music.yml +37 -0
  38. data/spec/cassettes/asin/lookup_and_search_should_search_never_mind_music.yml +122 -0
  39. data/spec/config_spec.rb +53 -0
  40. data/spec/search_spec.rb +101 -0
  41. data/spec/spec_helper.rb +47 -0
  42. metadata +197 -0
@@ -0,0 +1,3 @@
1
+ lib/**/*.rb
2
+ README.rdoc
3
+ CHANGELOG.rdoc
@@ -0,0 +1,6 @@
1
+ html
2
+ pkg
3
+ *.gem
4
+ .DS_Store
5
+ .bundle
6
+ .rvmrc
@@ -0,0 +1,13 @@
1
+ script: "bundle exec rake"
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - rbx
7
+ - rbx-2.0
8
+ - ree
9
+ - ruby-head
10
+ - jruby
11
+ branches:
12
+ only:
13
+ - develop
@@ -0,0 +1,49 @@
1
+ == 0.7.0
2
+
3
+ * jruby compatible
4
+ * loosen gem dependencies
5
+
6
+ == 0.6.1
7
+
8
+ * fix error when passing nil to config values
9
+
10
+ == 0.6.0
11
+
12
+ * change lookup method - pull request https://github.com/phoet/asin/pull/8
13
+
14
+ == 0.5.1
15
+
16
+ * fix for https://github.com/phoet/asin/issues/7
17
+
18
+ == 0.5.0
19
+
20
+ * move client to own file
21
+ * use autoload
22
+ * support for Hashie::Rash
23
+ * new method browse_node
24
+
25
+ == 0.4.0
26
+
27
+ * add configuration option for item/cart class
28
+ * add more functionality to item class
29
+
30
+ == 0.4.0.beta1
31
+
32
+ * added cart operations
33
+ * added yml configuration
34
+
35
+ == 0.3.0
36
+
37
+ * add search_keywords method
38
+ * open up search method to be more flexible
39
+
40
+ == 0.2.0
41
+
42
+ * rails initializer configuration
43
+ * rpsec for tests
44
+
45
+ == 0.1.0
46
+
47
+ * add logger
48
+ * use HTTPI as HTTP-adapter
49
+ * use bundler for dependencies
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in asin.gemspec
4
+ gemspec
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ asin (0.7.0)
5
+ crack (~> 0.3)
6
+ hashie (~> 1.1)
7
+ httpi (~> 0.9)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ addressable (2.2.6)
13
+ crack (0.3.1)
14
+ diff-lcs (1.1.3)
15
+ fuubar (0.0.6)
16
+ rspec (~> 2.0)
17
+ rspec-instafail (~> 0.1.8)
18
+ ruby-progressbar (~> 0.0.10)
19
+ hashie (1.1.0)
20
+ httpclient (2.2.3)
21
+ httpi (0.9.5)
22
+ rack
23
+ rack (1.4.1)
24
+ rake (0.9.2.2)
25
+ rash (0.3.1)
26
+ hashie (~> 1.1.0)
27
+ rspec (2.7.0)
28
+ rspec-core (~> 2.7.0)
29
+ rspec-expectations (~> 2.7.0)
30
+ rspec-mocks (~> 2.7.0)
31
+ rspec-core (2.7.1)
32
+ rspec-expectations (2.7.0)
33
+ diff-lcs (~> 1.1.2)
34
+ rspec-instafail (0.1.9)
35
+ rspec-mocks (2.7.0)
36
+ ruby-progressbar (0.0.10)
37
+ vcr (1.11.3)
38
+ webmock (1.7.7)
39
+ addressable (~> 2.2, > 2.2.5)
40
+ crack (>= 0.1.7)
41
+
42
+ PLATFORMS
43
+ java
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ asin!
48
+ fuubar (~> 0.0.5)
49
+ httpclient (~> 2.2.3)
50
+ rake (~> 0.9.2.2)
51
+ rash (~> 0.3.1)
52
+ rspec (~> 2.7.0)
53
+ vcr (~> 1.11.3)
54
+ webmock (~> 1.7.7)
@@ -0,0 +1,166 @@
1
+ == Infos
2
+
3
+ Status: http://stillmaintained.com/phoet/asin.png
4
+ Build: http://travis-ci.org/phoet/asin.png
5
+
6
+ ASIN is a simple, extensible wrapper for parts of the REST-API of Amazon Product Advertising API (aka Associates Web Service aka Amazon E-Commerce Service).
7
+
8
+ For more information on the REST calls, have a look at the whole Amazon E-Commerce-API[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html].
9
+
10
+ Have a look at the RDOC[http://rdoc.info/projects/phoet/asin] for this project, if you like browsing some docs.
11
+
12
+ The gem runs smoothly with Rails 3 and is tested against multiple rubies. See +.travis.yml+ for details.
13
+
14
+ == Installation
15
+
16
+ gem install asin
17
+ gem install httpclient # optional, see HTTPI
18
+ gem install rash # optional, see Response Configuration
19
+
20
+ or in your Gemfile:
21
+
22
+ gem 'asin'
23
+ gem 'httpclient' # optional, see HTTPI
24
+ gem 'rash' # optional, see Response Configuration
25
+
26
+ == Configuration
27
+
28
+ Rails style initializer (config/initializers/asin.rb):
29
+
30
+ ASIN::Configuration.configure do |config|
31
+ config.secret = 'your-secret'
32
+ config.key = 'your-key'
33
+ end
34
+
35
+ YAML style configuration:
36
+
37
+ ASIN::Configuration.configure :yaml => 'config/asin.yml'
38
+
39
+ Inline style configuration:
40
+
41
+ ASIN::Configuration.configure :secret => 'your-secret', :key => 'your-key'
42
+ # or
43
+ client.configure :secret => 'your-secret', :key => 'your-key'
44
+
45
+ Have a look at ASIN::Configuration class for all the details.
46
+
47
+ ===API Changes
48
+
49
+ With the latest version of the Product Advertising API you need to include your associate_tag[https://affiliate-program.amazon.com/gp/advertising/api/detail/api-changes.html].
50
+
51
+ ASIN::Configuration.configure do |config|
52
+ config.secret = 'your-secret'
53
+ config.key = 'your-key'
54
+ config.associate_tag = 'your-tag'
55
+ end
56
+
57
+ == Usage
58
+
59
+ ASIN is designed as a module, so you can include it into any object you like:
60
+
61
+ # require and include
62
+ require 'asin'
63
+ include ASIN::Client
64
+
65
+ # lookup an ASIN
66
+ lookup '1430218150'
67
+
68
+ But you can also use the +instance+ method to get a proxy-object:
69
+
70
+ # just require
71
+ require 'asin'
72
+
73
+ # create an ASIN client
74
+ client = ASIN::Client.instance
75
+
76
+ # lookup an item with the amazon standard identification number (asin)
77
+ items = client.lookup '1430218150'
78
+
79
+ # have a look at the title of the item
80
+ items.first.title
81
+ => Learn Objective-C on the Mac (Learn Series)
82
+
83
+ # search for any kind of stuff on amazon with keywords
84
+ items = search_keywords 'Learn', 'Objective-C'
85
+ items.first.title
86
+ => "Learn Objective-C on the Mac (Learn Series)"
87
+
88
+ # search for any kind of stuff on amazon with custom parameters
89
+ search :Keywords => 'Learn Objective-C', :SearchIndex => :Books
90
+ items.first.title
91
+ => "Learn Objective-C on the Mac (Learn Series)"
92
+
93
+ # access the internal data representation (Hashie::Mash)
94
+ item.raw.ItemAttributes.ListPrice.FormattedPrice
95
+ => $39.99
96
+
97
+ There is an additional set of methods to support AWS cart operations:
98
+
99
+ client = ASIN::Client.instance
100
+
101
+ # create a cart with an item
102
+ cart = client.create_cart({:asin => '1430218150', :quantity => 1})
103
+ cart.items
104
+ => [<#Hashie::Mash ASIN="1430218150" CartItemId="U3G241HVLLB8N6" ... >]
105
+
106
+ # get an already existing cart from a CartId and HMAC
107
+ cart = client.get_cart('176-9182855-2326919', 'KgeVCA0YJTbuN/7Ibakrk/KnHWA=')
108
+ cart.empty?
109
+ => false
110
+
111
+ # clear everything from the cart
112
+ cart = client.clear_cart(cart)
113
+ cart.empty?
114
+ => true
115
+
116
+ # add items to the cart
117
+ cart = client.add_items(cart, {:asin => '1430216263', :quantity => 2})
118
+ cart.empty?
119
+ => false
120
+
121
+ # update items in the cart
122
+ cart = client.update_items(cart, {:cart_item_id => cart.items.first.CartItemId, :action => :SaveForLater}, {:cart_item_id => cart.items.first.CartItemId, :quantity => 7})
123
+ cart.saved_items
124
+ => [<#Hashie::Mash ASIN="1430218150" CartItemId="U3G241HVLLB8N6" ... >]
125
+
126
+ It's also possible to access browse nodes:
127
+
128
+ client = ASIN::Client.instance
129
+
130
+ # create a cart with an item
131
+ node = client.browse_node('163357', :ResponseGroup => :TopSellers)
132
+ node.node_id
133
+ => '163357'
134
+ node.name
135
+ => 'Comedy'
136
+
137
+ == Response Configuration
138
+
139
+ ASIN is customizable in the way it returns Responses from Amazon.
140
+ By default it will return +SimpleItem+, +SimpleCart+ or +SimpleNode+ instances,
141
+ but you can override this behavior for using your custom Classes:
142
+
143
+ client.configure :item_type => YourItemClass
144
+ client.configure :cart_type => YourCartClass
145
+ client.configure :node_type => YourNodeClass
146
+
147
+ You can also use built-in +:raw+, +:mash+ or +:rash+ types.
148
+ Since +rash+ is an additional library, you need to add it to your gemfile if you want to use it:
149
+
150
+ gem 'rash'
151
+
152
+ == HTTPI
153
+
154
+ ASIN uses HTTPI[https://github.com/rubiii/httpi] as a HTTP-Client adapter.
155
+ See the HTTPI documentation for how to configure different clients or the logger.
156
+ As a default HTTPI uses _httpclient_ so you should add that dependency to your project:
157
+
158
+ gem 'httpclient'
159
+
160
+
161
+ == License
162
+
163
+ "THE BEER-WARE LICENSE" (Revision 42):
164
+ ps@nofail.de[mailto:ps@nofail.de] wrote this file. As long as you retain this notice you
165
+ can do whatever you want with this stuff. If we meet some day, and you think
166
+ this stuff is worth it, you can buy me a beer in return Peter Schröder
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "asin/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rasin"
7
+ s.version = ASIN::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Peter Schröder']
10
+ s.email = ['phoetmail@googlemail.com']
11
+ s.homepage = 'http://github.com/phoet/asin'
12
+ s.summary = 'Simple interface to AWS Lookup, Search and Cart operations.'
13
+ s.description = 'Amazon Simple INterface.'
14
+
15
+ s.rubyforge_project = "asin"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency('crack', '~> 0.3')
23
+ s.add_dependency('hashie', '~> 1.1')
24
+ s.add_dependency('httpi', '~> 0.9')
25
+
26
+ s.add_runtime_dependency('jruby-openssl') if RUBY_PLATFORM == 'java'
27
+
28
+ s.add_development_dependency('httpclient', '~> 2.2.3')
29
+ s.add_development_dependency('rash', '~> 0.3.1')
30
+
31
+ s.add_development_dependency('rake', '~> 0.9.2.2')
32
+ s.add_development_dependency('vcr', '~> 1.11.3')
33
+ s.add_development_dependency('webmock', '~> 1.7.7')
34
+ s.add_development_dependency('rspec', '~> 2.7.0')
35
+ s.add_development_dependency('fuubar', '~> 0.0.5')
36
+ end
@@ -0,0 +1,8 @@
1
+ module ASIN
2
+ autoload :Client, 'asin/client'
3
+ autoload :SimpleItem, 'asin/simple_item'
4
+ autoload :SimpleCart, 'asin/simple_cart'
5
+ autoload :SimpleNode, 'asin/simple_node'
6
+ autoload :Version, 'asin/version'
7
+ autoload :Configuration, 'asin/configuration'
8
+ end
@@ -0,0 +1,398 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'httpi'
3
+ require 'crack/xml'
4
+ require 'cgi'
5
+ require 'base64'
6
+
7
+ # ASIN (Amazon Simple INterface) is a gem for easy access of the Amazon E-Commerce-API.
8
+ # It is simple to configure and use. Since it's very small and flexible, it is easy to extend it to your needs.
9
+ #
10
+ # Author:: Peter Schröder (mailto:phoetmail@googlemail.com)
11
+ #
12
+ # == Usage
13
+ #
14
+ # The ASIN module is designed as a mixin.
15
+ #
16
+ # require 'asin'
17
+ # include ASIN::Client
18
+ #
19
+ # In order to use the Amazon API properly, you need to be a registered user (http://aws.amazon.com).
20
+ #
21
+ # The registration process will give you a +secret-key+ and an +access-key+ (AWSAccessKeyId).
22
+ #
23
+ # Both are needed to use ASIN (see Configuration for more details):
24
+ #
25
+ # configure :secret => 'your-secret', :key => 'your-key'
26
+ #
27
+ # == Search
28
+ #
29
+ # After configuring your environment you can call the +lookup+ method to retrieve an +SimpleItem+ via the
30
+ # Amazon Standard Identification Number (ASIN):
31
+ #
32
+ # item = lookup '1430218150'
33
+ # item.first.title
34
+ # => "Learn Objective-C on the Mac (Learn Series)"
35
+ #
36
+ # OR search with fulltext/ASIN/ISBN
37
+ #
38
+ # items = search 'Learn Objective-C'
39
+ # items.first.title
40
+ # => "Learn Objective-C on the Mac (Learn Series)"
41
+ #
42
+ # The +SimpleItem+ uses a Hashie::Mash as its internal data representation and you can get fetched data from it:
43
+ #
44
+ # item.raw.ItemAttributes.ListPrice.FormattedPrice
45
+ # => "$39.99"
46
+ #
47
+ # == Further Configuration
48
+ #
49
+ # If you need more controll over the request that is sent to the
50
+ # Amazon API (http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html),
51
+ # you can override some defaults or add additional query-parameters to the REST calls:
52
+ #
53
+ # configure :host => 'webservices.amazon.de'
54
+ # lookup(asin, :ResponseGroup => :Medium)
55
+ #
56
+ # == Cart
57
+ #
58
+ # ASIN helps with AWS cart-operations.
59
+ # It currently supports the CartCreate, CartGet, CartAdd, CartModify and CartClear operations:
60
+ #
61
+ # cart = create_cart({:asin => '1430218150', :quantity => 1})
62
+ # cart.valid?
63
+ # cart.items
64
+ # => true
65
+ # => [<#Hashie::Mash ASIN="1430218150" CartItemId="U3G241HVLLB8N6" ... >]
66
+ #
67
+ # cart = get_cart('176-9182855-2326919', 'KgeVCA0YJTbuN/7Ibakrk/KnHWA=')
68
+ # cart.empty?
69
+ # => false
70
+ #
71
+ # cart = clear_cart(cart)
72
+ # cart.empty?
73
+ # => true
74
+ #
75
+ # cart = add_items(cart, {:asin => '1430216263', :quantity => 2})
76
+ # cart.empty?
77
+ # => false
78
+ #
79
+ # cart = update_items(cart, {:cart_item_id => cart.items.first.CartItemId, :action => :SaveForLater}, {:cart_item_id => cart.items.first.CartItemId, :quantity => 7})
80
+ # cart.valid?
81
+ # cart.saved_items
82
+ # => true
83
+ # => [<#Hashie::Mash ASIN="1430218150" CartItemId="U3G241HVLLB8N6" ... >]
84
+ #
85
+ # == Nodes
86
+ #
87
+ # In order to browse Amazon nodes, you can use +browse_node+ method:
88
+ #
89
+ # node = browse_node('163357')
90
+ # node.node_id
91
+ # => '163357'
92
+ # node.name
93
+ # => 'Comedy'
94
+ # node.children
95
+ # node.ancestors
96
+ #
97
+ # you can configure the +:ResponseGroup+ option to your needs:
98
+ #
99
+ # node = browse_node('163357', :ResponseGroup => :TopSellers)
100
+ #
101
+ module ASIN
102
+ module Client
103
+
104
+ DIGEST = OpenSSL::Digest::Digest.new('sha256')
105
+ PATH = '/onca/xml'
106
+
107
+ # Convenience method to create an ASIN client.
108
+ #
109
+ # An instance is not necessary though, you can simply include the ASIN module otherwise.
110
+ #
111
+ def self.instance
112
+ ins = Object.new
113
+ ins.extend ASIN::Client
114
+ ins
115
+ end
116
+
117
+ # Configures the basic request parameters for ASIN.
118
+ #
119
+ # Expects at least +secret+ and +key+ for the API call:
120
+ #
121
+ # configure :secret => 'your-secret', :key => 'your-key'
122
+ #
123
+ # See ASIN::Configuration for more infos.
124
+ #
125
+ def configure(options={})
126
+ Configuration.configure(options)
127
+ end
128
+
129
+ # Performs an +ItemLookup+ REST call against the Amazon API.
130
+ #
131
+ # Expects an arbitrary number of ASIN (Amazon Standard Identification Number) and returns an array of +SimpleItem+:
132
+ #
133
+ # item = lookup '1430218150'
134
+ # item.title
135
+ # => "Learn Objective-C on the Mac (Learn Series)"
136
+ # items = lookup ['1430218150', '0439023521']
137
+ # items[0].title
138
+ # => "Learn Objective-C on the Mac (Learn Series)"
139
+ # items[1].title
140
+ # => "The Hunger Games"
141
+ #
142
+ # ==== Options:
143
+ #
144
+ # Additional parameters for the API call like this:
145
+ #
146
+ # lookup(asin, :ResponseGroup => :Medium)
147
+ #
148
+ # Or with multiple parameters:
149
+ #
150
+ # lookup(asin, :ResponseGroup => [:Small, :AlternateVersions])
151
+ #
152
+ def lookup(*asins)
153
+ params = asins.last.is_a?(Hash) ? asins.pop : {:ResponseGroup => :Medium}
154
+ response = call(params.merge(:Operation => :ItemLookup, :ItemId => asins.join(',')))
155
+ [arrayfy(response['ItemLookupResponse']['Items']['Item']).map {|item| handle_item(item)},response]
156
+ end
157
+
158
+ # Performs an +ItemSearch+ REST call against the Amazon API.
159
+ #
160
+ # Expects a search-string which can be an arbitrary array of strings (ASINs f.e.) and returns a list of +SimpleItem+s:
161
+ #
162
+ # items = search_keywords 'Learn', 'Objective-C'
163
+ # items.first.title
164
+ # => "Learn Objective-C on the Mac (Learn Series)"
165
+ #
166
+ # ==== Options:
167
+ #
168
+ # Additional parameters for the API call like this:
169
+ #
170
+ # search_keywords('nirvana', 'never mind', :SearchIndex => :Music)
171
+ #
172
+ # Have a look at the different search index values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
173
+ #
174
+ def search_keywords(*keywords)
175
+ params = keywords.last.is_a?(Hash) ? keywords.pop : {:SearchIndex => :Books, :ResponseGroup => :Medium}
176
+ response = call(params.merge(:Operation => :ItemSearch, :Keywords => keywords.join(' ')))
177
+
178
+ [arrayfy(response['ItemSearchResponse']['Items']['Item']).map {|item| handle_item(item)},response]
179
+ end
180
+
181
+ # Performs an +ItemSearch+ REST call against the Amazon API.
182
+ #
183
+ # Expects a Hash of search params where and returns a list of +SimpleItem+s:
184
+ #
185
+ # items = search :SearchIndex => :Music
186
+ #
187
+ # ==== Options:
188
+ #
189
+ # Additional parameters for the API call like this:
190
+ #
191
+ # search(:Keywords => 'nirvana', :SearchIndex => :Music)
192
+ #
193
+ # Have a look at the different search index values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
194
+ #
195
+ def search(params={:SearchIndex => :Books, :ResponseGroup => :Medium})
196
+ response = call(params.merge(:Operation => :ItemSearch))
197
+ [arrayfy(response['ItemSearchResponse']['Items']['Item']).map {|item| handle_item(item)},response]
198
+ end
199
+
200
+ # Performs an +BrowseNodeLookup+ REST call against the Amazon API.
201
+ #
202
+ # Expects a Hash of search params where and returns a +SimpleNode+:
203
+ #
204
+ # node = browse_node '163357'
205
+ #
206
+ # ==== Options:
207
+ #
208
+ # Additional parameters for the API call like this:
209
+ #
210
+ # browse_node('163357', :ResponseGroup => :TopSellers)
211
+ #
212
+ # Have a look at the different browse node values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
213
+ #
214
+ def browse_node(node_id, params={:ResponseGroup => :BrowseNodeInfo})
215
+ response = call(params.merge(:Operation => :BrowseNodeLookup, :BrowseNodeId => node_id))
216
+ [handle_type(response['BrowseNodeLookupResponse']['BrowseNodes']['BrowseNode'], Configuration.node_type),response]
217
+ end
218
+
219
+ # Performs an +CartCreate+ REST call against the Amazon API.
220
+ #
221
+ # Expects one ore more item-hashes and returns a +SimpleCart+:
222
+ #
223
+ # cart = create_cart({:asin => '1430218150', :quantity => 1})
224
+ #
225
+ # ==== Options:
226
+ #
227
+ # Additional parameters for the API call like this:
228
+ #
229
+ # create_cart({:asin => '1430218150', :quantity => 1}, {:asin => '1430216263', :quantity => 1, :action => :SaveForLater})
230
+ #
231
+ # Have a look at the different cart item operation values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
232
+ #
233
+ def create_cart(*items)
234
+ cart(:CartCreate, create_item_params(items))
235
+ end
236
+
237
+ # Performs an +CartGet+ REST call against the Amazon API.
238
+ #
239
+ # Expects the CartId and the HMAC to identify the returning +SimpleCart+:
240
+ #
241
+ # cart = get_cart('176-9182855-2326919', 'KgeVCA0YJTbuN/7Ibakrk/KnHWA=')
242
+ #
243
+ def get_cart(cart_id, hmac)
244
+ cart(:CartGet, {:CartId => cart_id, :HMAC => hmac})
245
+ end
246
+
247
+ # Performs an +CartAdd+ REST call against the Amazon API.
248
+ #
249
+ # Expects a +SimpleCart+ created with +create_cart+ and one ore more Item-Hashes and returns an updated +SimpleCart+:
250
+ #
251
+ # cart = add_items(cart, {:asin => '1430216263', :quantity => 2})
252
+ #
253
+ # ==== Options:
254
+ #
255
+ # Additional parameters for the API call like this:
256
+ #
257
+ # add_items(cart, {:asin => '1430218150', :quantity => 1}, {:asin => '1430216263', :quantity => 1, :action => :SaveForLater})
258
+ #
259
+ # Have a look at the different cart item operation values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
260
+ #
261
+ def add_items(cart, *items)
262
+ cart(:CartAdd, create_item_params(items).merge({:CartId => cart.cart_id, :HMAC => cart.hmac}))
263
+ end
264
+
265
+ # Performs an +CartModify+ REST call against the Amazon API.
266
+ #
267
+ # Expects a +SimpleCart+ created with +create_cart+ and one ore more Item-Hashes to modify and returns an updated +SimpleCart+:
268
+ #
269
+ # cart = update_items(cart, {:cart_item_id => cart.items.first.CartItemId, :quantity => 7})
270
+ #
271
+ # ==== Options:
272
+ #
273
+ # Additional parameters for the API call like this:
274
+ #
275
+ # update_items(cart, {:cart_item_id => cart.items.first.CartItemId, :action => :SaveForLater}, {:cart_item_id => cart.items.first.CartItemId, :quantity => 7})
276
+ #
277
+ # Have a look at the different cart item operation values on the Amazon-Documentation[http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html]
278
+ #
279
+ def update_items(cart, *items)
280
+ cart(:CartModify, create_item_params(items).merge({:CartId => cart.cart_id, :HMAC => cart.hmac}))
281
+ end
282
+
283
+ # Performs an +CartClear+ REST call against the Amazon API.
284
+ #
285
+ # Expects a +SimpleCart+ created with +create_cart+ and returns an empty +SimpleCart+:
286
+ #
287
+ # cart = clear_cart(cart)
288
+ #
289
+ def clear_cart(cart)
290
+ cart(:CartClear, {:CartId => cart.cart_id, :HMAC => cart.hmac})
291
+ end
292
+
293
+ private()
294
+
295
+ def arrayfy(item)
296
+ return [] unless item
297
+ item.is_a?(Array) ? item : [item]
298
+ end
299
+
300
+ def handle_item(item)
301
+ handle_type(item, Configuration.item_type)
302
+ end
303
+
304
+ def handle_type(data, type)
305
+ if type.is_a?(Class)
306
+ type.new(data)
307
+ elsif type == :mash
308
+ require 'hashie'
309
+ Hashie::Mash.new(data)
310
+ elsif type == :rash
311
+ require 'rash'
312
+ Hashie::Rash.new(data)
313
+ else
314
+ data
315
+ end
316
+ end
317
+
318
+ def create_item_params(items)
319
+ keyword_mappings = {
320
+ :asin => 'ASIN',
321
+ :quantity => 'Quantity',
322
+ :cart_item_id => 'CartItemId',
323
+ :offer_listing_id => 'OfferListingId',
324
+ :action => 'Action'
325
+ }
326
+ params = {}
327
+ items.each_with_index do |item, i|
328
+ item.each do |key, value|
329
+ next unless keyword = keyword_mappings[key]
330
+ params["Item.#{i}.#{keyword}"] = value.to_s
331
+ end
332
+ end
333
+ params
334
+ end
335
+
336
+ def cart(operation, params={})
337
+ response = call(params.merge(:Operation => operation))
338
+ cart = response["#{operation}Response"]['Cart']
339
+ handle_type(cart, Configuration.cart_type)
340
+ end
341
+
342
+ def call(params)
343
+ Configuration.validate_credentials!
344
+ params[:ResponseGroup] = params[:ResponseGroup].collect{|g| g.to_s.strip}.join(',') if !params[:ResponseGroup].nil? && params[:ResponseGroup].is_a?(Array)
345
+ log(:debug, "calling with params=#{params}")
346
+ signed = create_signed_query_string(params)
347
+
348
+ url = "http://#{Configuration.host}#{PATH}?#{signed}"
349
+ log(:info, "performing rest call to url='#{url}'")
350
+
351
+ response = HTTPI.get(url)
352
+ if response.code == 200
353
+ # force utf-8 chars, works only on 1.9 string
354
+ resp = response.body
355
+ resp = resp.force_encoding('UTF-8') if resp.respond_to? :force_encoding
356
+ log(:debug, "got response='#{resp}'")
357
+ Crack::XML.parse(resp)
358
+ else
359
+ log(:error, "got response='#{response.body}'")
360
+ raise "request failed with response-code='#{response.code}'"
361
+ end
362
+ end
363
+
364
+ def create_signed_query_string(params)
365
+ # nice tutorial http://cloudcarpenters.com/blog/amazon_products_api_request_signing/
366
+ params[:Service] = :AWSECommerceService
367
+ params[:AWSAccessKeyId] = Configuration.key
368
+
369
+ params[:Version] = Configuration.version unless Configuration.blank? :version
370
+ params[:AssociateTag] = Configuration.associate_tag unless Configuration.blank? :associate_tag
371
+
372
+ # utc timestamp needed for signing
373
+ params[:Timestamp] = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
374
+
375
+
376
+ query = create_query(params)
377
+
378
+ # yeah, you really need to sign the get-request not the query
379
+ request_to_sign = "GET\n#{Configuration.host}\n#{PATH}\n#{query}"
380
+ hmac = OpenSSL::HMAC.digest(DIGEST, Configuration.secret, request_to_sign)
381
+
382
+ # don't forget to remove the newline from base64
383
+ signature = CGI.escape(Base64.encode64(hmac).chomp)
384
+ "#{query}&Signature=#{signature}"
385
+ end
386
+
387
+ def create_query(params)
388
+ params.map do |key, value|
389
+ value = value.collect{|v| v.to_s.strip}.join(',') if value.is_a?(Array)
390
+ "#{key}=#{CGI.escape(value.to_s)}"
391
+ end.sort.join('&').gsub('+','%20') # signing needs to order the query alphabetically
392
+ end
393
+
394
+ def log(severity, message)
395
+ Configuration.logger.send severity, message if Configuration.logger
396
+ end
397
+ end
398
+ end