rasin 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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