vacuum 0.1.3 → 0.2.0.pre

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 (41) hide show
  1. data/README.md +69 -27
  2. data/lib/vacuum/endpoint/base.rb +58 -0
  3. data/lib/vacuum/endpoint/mws.rb +59 -0
  4. data/lib/vacuum/endpoint/product_advertising.rb +34 -0
  5. data/lib/vacuum/mws.rb +8 -0
  6. data/lib/vacuum/product_advertising.rb +7 -0
  7. data/lib/vacuum/request/base.rb +96 -0
  8. data/lib/vacuum/request/mws.rb +24 -0
  9. data/lib/vacuum/request/product_advertising.rb +90 -0
  10. data/lib/vacuum/request/signature/authentication.rb +32 -0
  11. data/lib/vacuum/request/signature/builder.rb +70 -0
  12. data/lib/vacuum/request/utils.rb +24 -0
  13. data/lib/vacuum/response/base.rb +57 -0
  14. data/lib/vacuum/response/mws.rb +7 -0
  15. data/lib/vacuum/response/product_advertising.rb +19 -0
  16. data/lib/vacuum/response/utils.rb +48 -0
  17. data/lib/vacuum/version.rb +1 -1
  18. data/lib/vacuum.rb +31 -11
  19. data/spec/fixtures/{http_response → product_advertising} +0 -0
  20. data/spec/spec_helper.rb +5 -4
  21. data/spec/support/shared_examples/endpoint.rb +43 -0
  22. data/spec/support/shared_examples/request.rb +95 -0
  23. data/spec/support/shared_examples/response.rb +51 -0
  24. data/spec/vacuum/endpoint/base_spec.rb +19 -0
  25. data/spec/vacuum/endpoint/mws_spec.rb +47 -0
  26. data/spec/vacuum/endpoint/product_advertising_spec.rb +25 -0
  27. data/spec/vacuum/request/base_spec.rb +22 -0
  28. data/spec/vacuum/request/mws_spec.rb +26 -0
  29. data/spec/vacuum/request/product_advertising_spec.rb +104 -0
  30. data/spec/vacuum/request/signature/authentication_spec.rb +30 -0
  31. data/spec/vacuum/request/signature/builder_spec.rb +76 -0
  32. data/spec/vacuum/request/utils_spec.rb +23 -0
  33. data/spec/vacuum/response/base_spec.rb +38 -0
  34. data/spec/vacuum/response/product_advertising_spec.rb +57 -0
  35. data/spec/vacuum/response/utils_spec.rb +42 -0
  36. metadata +107 -21
  37. data/lib/vacuum/em.rb +0 -21
  38. data/lib/vacuum/request.rb +0 -119
  39. data/lib/vacuum/response.rb +0 -63
  40. data/spec/vacuum/request_spec.rb +0 -130
  41. data/spec/vacuum/response_spec.rb +0 -80
@@ -0,0 +1,19 @@
1
+ module Vacuum
2
+ module Response
3
+ # A Product Advertising API response.
4
+ class ProductAdvertising < Base
5
+ # Returns an Array of errors.
6
+ def errors
7
+ find 'Error'
8
+ end
9
+
10
+ # Returns whether the response has errors.
11
+ #
12
+ # Note that a response with errors is still technically valid, i.e. the
13
+ # response code is 200.
14
+ def has_errors?
15
+ errors.count > 0
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ module Vacuum
2
+ module Response
3
+ module Utils
4
+ # Builds a Hash from an XML document.
5
+ #
6
+ # xml - a Nokogiri::XML::Document or Nokogiri::XML::Element.
7
+ #
8
+ # Returns a Hash representation of the XML document.
9
+ def self.xml_to_hash(xml)
10
+ case xml
11
+ when Nokogiri::XML::Document
12
+ xml_to_hash xml.root
13
+ when Nokogiri::XML::Element
14
+ hsh = {}
15
+
16
+ xml.attributes.each_pair do |key, attribute|
17
+ hsh[key] = attribute.value
18
+ end
19
+
20
+ xml.children.each do |child|
21
+ result = xml_to_hash child
22
+
23
+ if child.name == 'text'
24
+ if hsh.empty?
25
+ return result
26
+ else
27
+ hsh['__content__'] = result
28
+ end
29
+ elsif hsh[child.name]
30
+ case hsh[child.name]
31
+ when Array
32
+ hsh[child.name] << result
33
+ else
34
+ hsh[child.name] = [hsh[child.name]] << result
35
+ end
36
+ else
37
+ hsh[child.name] = result
38
+ end
39
+ end
40
+
41
+ hsh
42
+ else
43
+ xml.content.to_s
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module Vacuum
2
- VERSION = '0.1.3'
2
+ VERSION = '0.2.0.pre'
3
3
  end
data/lib/vacuum.rb CHANGED
@@ -1,25 +1,45 @@
1
+ # Standard library dependencies.
2
+ require 'base64'
1
3
  require 'forwardable'
2
- require 'net/http'
3
4
  require 'openssl'
4
5
  require 'time'
5
6
 
6
- require 'knack'
7
-
8
- %w{request response}.each do |f|
9
- require "vacuum/#{f}"
10
- end
7
+ # External dependencies.
8
+ require 'addressable/uri'
9
+ require 'faraday'
10
+ require 'nokogiri'
11
11
 
12
+ # Internal dependencies.
13
+ require 'vacuum/endpoint/base'
14
+ require 'vacuum/request/base'
15
+ require 'vacuum/request/signature/authentication'
16
+ require 'vacuum/request/signature/builder'
17
+ require 'vacuum/request/utils'
18
+ require 'vacuum/response/base'
19
+ require 'vacuum/response/utils'
20
+ require 'vacuum/version'
12
21
 
13
- # Vacuum is a Ruby wrapper to the Amazon Product Advertising API.
22
+ # Vacuum is a Ruby wrapper to various Amazon Web Services (AWS) APIs.
14
23
  module Vacuum
15
24
  class BadLocale < ArgumentError; end
16
25
  class MissingKey < ArgumentError; end
17
26
  class MissingSecret < ArgumentError; end
18
- class MissingTag < ArgumentError; end
19
27
 
20
28
  class << self
21
- extend Forwardable
22
-
23
- def_delegators Request, :new
29
+ def new(api, &blk)
30
+ case api
31
+ when /^mws/
32
+ require 'vacuum/mws'
33
+ Request::MWS.new do |config|
34
+ config.api = api.slice(4, api.size).to_sym
35
+ blk.call config
36
+ end
37
+ when :product_advertising
38
+ require 'vacuum/product_advertising'
39
+ Request::ProductAdvertising.new &blk
40
+ else
41
+ raise NotImplementedError
42
+ end
43
+ end
24
44
  end
25
45
  end
data/spec/spec_helper.rb CHANGED
@@ -1,14 +1,15 @@
1
- require 'rubygems'
2
- require 'bundler/setup'
3
1
  require 'rspec'
4
-
5
2
  begin
6
3
  require 'pry'
7
4
  rescue LoadError
8
5
  end
9
6
 
10
- require File.expand_path('../../lib/vacuum', __FILE__)
7
+ require 'vacuum'
8
+ require 'vacuum/mws'
9
+ require 'vacuum/product_advertising'
11
10
 
12
11
  RSpec.configure do |c|
13
12
  c.treat_symbols_as_metadata_keys_with_true_values = true
14
13
  end
14
+
15
+ Dir['./spec/support/**/*.rb'].each { |f| require f }
@@ -0,0 +1,43 @@
1
+ shared_examples 'an endpoint' do
2
+ describe '#key' do
3
+ it 'requires key to have been set' do
4
+ expect { endpoint.key }.to raise_error Vacuum::MissingKey
5
+ end
6
+ end
7
+
8
+ describe '#locale' do
9
+ it 'defaults to the US' do
10
+ endpoint.locale.should eql 'US'
11
+ end
12
+
13
+ context 'if not valid' do
14
+ before do
15
+ endpoint.locale = 'foo'
16
+ end
17
+
18
+ it 'raises an error' do
19
+ expect { endpoint.locale }.to raise_error Vacuum::BadLocale
20
+ end
21
+ end
22
+ end
23
+
24
+ describe '#secret' do
25
+ it 'requires secret to have been set' do
26
+ expect { endpoint.secret }.to raise_error Vacuum::MissingSecret
27
+ end
28
+ end
29
+
30
+ describe '#user_agent' do
31
+ it 'includes the library version number' do
32
+ endpoint.user_agent.should match /Vacuum\/[\d\w.]+\s/
33
+ end
34
+
35
+ it 'describes the Ruby interpreter' do
36
+ endpoint.user_agent.should match /Language=(?:j?ruby|rbx)/
37
+ end
38
+
39
+ it 'describes the host' do
40
+ endpoint.user_agent.should match /Host=[\w\d]+/
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,95 @@
1
+ shared_examples 'a request' do
2
+ describe '#connection' do
3
+ let(:middleware) do
4
+ request.connection.builder.handlers
5
+ end
6
+
7
+ it 'returns a Faraday Connection' do
8
+ request.connection.should be_a Faraday::Connection
9
+ end
10
+
11
+ it 'adds Signature Authentication to middleware stack' do
12
+ middleware.should include Vacuum::Request::Signature::Authentication
13
+ end
14
+
15
+ it 'yields a builder' do
16
+ klass = Class.new Faraday::Middleware
17
+ request.connection do |builder|
18
+ builder.use klass
19
+ end
20
+ middleware.should include klass
21
+ end
22
+ end
23
+
24
+ describe '#endpoint' do
25
+ it 'returns an AWS API endpoint' do
26
+ request.endpoint.should be_a Vacuum::Endpoint::Base
27
+ end
28
+ end
29
+
30
+ describe '#build' do
31
+ it 'merges passed key and value pairs into the parameters' do
32
+ request.build 'Key' => 'value'
33
+ request.parameters['Key'].should eql 'value'
34
+ end
35
+
36
+ it 'casts Symbol keys to camel-cased String' do
37
+ request.build :foo_bar => 'value'
38
+ request.parameters.should have_key 'FooBar'
39
+ end
40
+
41
+ it 'does not modify String keys' do
42
+ request.build 'foo_bar' => 'value'
43
+ request.parameters.should have_key 'foo_bar'
44
+ end
45
+
46
+ it 'casts values to String' do
47
+ request.build 'Key' => 1
48
+ request.parameters['Key'].should eql '1'
49
+ end
50
+
51
+ it 'concatenates Array values with a comma' do
52
+ request.build 'Key' => ['foo', 'bar']
53
+ request.parameters['Key'].should eql 'foo,bar'
54
+ end
55
+
56
+ it 'returns self' do
57
+ request.build({}).should eql request
58
+ end
59
+ end
60
+
61
+ describe '#build!' do
62
+ it 'clears existing parameters' do
63
+ request.build 'Key' => 'value'
64
+ request.build!.parameters.should_not have_key 'Key'
65
+ end
66
+ end
67
+
68
+ describe '#configure' do
69
+ it 'configures the AWS API endpoint' do
70
+ request.configure do |config|
71
+ config.locale = 'JP'
72
+ end
73
+ request.endpoint.locale.should eql 'JP'
74
+ end
75
+ end
76
+
77
+ describe '#get' do
78
+ context 'given an implemented url' do
79
+ before do
80
+ request.stub!(:url).and_return Addressable::URI.parse 'http://example.com'
81
+ end
82
+
83
+ it 'returns a Response' do
84
+ request.get.should be_a Vacuum::Response::Base
85
+ end
86
+ end
87
+ end
88
+
89
+ describe '#parameters' do
90
+ it 'includes default parameters' do
91
+ request.parameters.should have_key 'AWSAccessKeyId'
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,51 @@
1
+ shared_examples 'a response' do
2
+ let(:child_name) do
3
+ response.xml.children.first.name
4
+ end
5
+
6
+ describe '#find' do
7
+ it 'returns an Array of matches' do
8
+ response.find(child_name).should_not be_empty
9
+ end
10
+
11
+ it 'yields matches to a block' do
12
+ yielded = false
13
+ response.find(child_name) { yielded = true }
14
+ yielded.should be_true
15
+ end
16
+
17
+ it 'is aliased to []' do
18
+ response.find(child_name).should eql response[child_name]
19
+ end
20
+ end
21
+
22
+ describe '#to_hash' do
23
+ it 'casts response to a hash' do
24
+ response.to_hash.should be_a Hash
25
+ end
26
+ end
27
+
28
+ describe '#valid?' do
29
+ context 'when HTTP status is OK' do
30
+ it 'returns true' do
31
+ response.should be_valid
32
+ end
33
+ end
34
+
35
+ context 'when HTTP status is not OK' do
36
+ before do
37
+ response.code = 403
38
+ end
39
+
40
+ it 'returns false' do
41
+ response.should_not be_valid
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#xml' do
47
+ it 'returns a Nokogiri document' do
48
+ response.xml.should be_an_instance_of Nokogiri::XML::Document
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Endpoint
5
+ describe Base do
6
+ let(:endpoint) do
7
+ described_class.new
8
+ end
9
+
10
+ it_behaves_like 'an endpoint'
11
+
12
+ describe '#host' do
13
+ it 'is not implemented' do
14
+ expect { endpoint.host }.to raise_error NotImplementedError
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Endpoint
5
+ describe MWS do
6
+ let(:endpoint) do
7
+ described_class.new
8
+ end
9
+
10
+ describe '#host' do
11
+ it 'returns a host' do
12
+ endpoint.host.should match /amazon/
13
+ end
14
+ end
15
+
16
+ describe '#marketplace' do
17
+ it 'requires marketplace ID to have been set' do
18
+ expect { endpoint.marketplace }.to raise_error MissingMarketplace
19
+ end
20
+ end
21
+
22
+ describe '#seller' do
23
+ it 'requires seller ID to have been set' do
24
+ expect { endpoint.seller }.to raise_error MissingSeller
25
+ end
26
+ end
27
+
28
+ describe '#path' do
29
+ context 'when API type is set to Products' do
30
+ before do
31
+ endpoint.api = :products
32
+ end
33
+
34
+ it 'returns a URL path' do
35
+ endpoint.path.should match %r{/\w+/\w+}
36
+ end
37
+ end
38
+
39
+ context 'when API type is not implemented' do
40
+ it 'raises a Not Implemented Error' do
41
+ expect { endpoint.path }.to raise_error NotImplementedError
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Endpoint
5
+ describe ProductAdvertising do
6
+ let(:endpoint) do
7
+ described_class.new
8
+ end
9
+
10
+ it_behaves_like 'an endpoint'
11
+
12
+ describe '#host' do
13
+ it 'returns a host' do
14
+ endpoint.host.should match /amazon/
15
+ end
16
+ end
17
+
18
+ describe '#tag' do
19
+ it 'requires tag to have been set' do
20
+ expect { endpoint.tag }.to raise_error MissingTag
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Request
5
+ describe Base do
6
+ let(:request) do
7
+ described_class.new do |config|
8
+ config.key = 'key'
9
+ config.secret = 'secret'
10
+ end
11
+ end
12
+
13
+ it_behaves_like 'a request'
14
+
15
+ describe '#url' do
16
+ it 'is not implemented' do
17
+ expect { request.url }.to raise_error NotImplementedError
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Request
5
+ describe MWS do
6
+ let(:request) do
7
+ described_class.new do |config|
8
+ config.key = 'key'
9
+ config.marketplace = 'marketplace'
10
+ config.secret = 'secret'
11
+ config.seller = 'seller'
12
+ end
13
+ end
14
+
15
+ context 'when API type is set to Products' do
16
+ before do
17
+ request.configure do |config|
18
+ config.api = :products
19
+ end
20
+ end
21
+
22
+ it_behaves_like 'a request'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Request
5
+ describe ProductAdvertising do
6
+ let(:request) do
7
+ described_class.new do |config|
8
+ config.key = 'key'
9
+ config.secret = 'secret'
10
+ config.tag = 'tag'
11
+ end
12
+ end
13
+
14
+ it_behaves_like 'a request'
15
+
16
+ describe '#find' do
17
+ before do
18
+ request.stub! :get
19
+ end
20
+
21
+ let(:parameters) do
22
+ request.parameters
23
+ end
24
+
25
+ context 'given no items' do
26
+ it 'raises an error' do
27
+ expect { request.find }.to raise_error ArgumentError
28
+ end
29
+ end
30
+
31
+ context 'given up to 10 items' do
32
+ before do
33
+ request.find *(1..10), :foo => 'bar'
34
+ end
35
+
36
+ it 'builds a single-batch query' do
37
+ parameters['ItemId'].split(',').should =~ (1..10).map(&:to_s)
38
+ end
39
+
40
+ it 'takes parameters' do
41
+ parameters['Foo'].should eql 'bar'
42
+ end
43
+ end
44
+
45
+ context 'given 11 to to 20 items' do
46
+ before do
47
+ request.find *(1..20), :foo => 'bar'
48
+ end
49
+
50
+ it 'builds a multi-batch query' do
51
+ first_batch = parameters['ItemId.1.ItemId'].split(',')
52
+ first_batch.should =~ (1..10).map(&:to_s)
53
+ second_batch = parameters['ItemId.2.ItemId'].split(',')
54
+ second_batch.should =~ (11..20).map(&:to_s)
55
+ end
56
+
57
+ it 'takes parameters' do
58
+ parameters['ItemLookup.Shared.Foo'].should eql 'bar'
59
+ end
60
+ end
61
+
62
+ context 'given over 20 items' do
63
+ it 'raises an error' do
64
+ expect { request.find *(1..21) }.to raise_error ArgumentError
65
+ end
66
+ end
67
+ end
68
+
69
+ describe '#search' do
70
+ let(:parameters) do
71
+ request.parameters
72
+ end
73
+
74
+ context 'when given a search index and a keyword' do
75
+ before do
76
+ request.search :foo, 'bar'
77
+ end
78
+
79
+ it 'builds a keyword search' do
80
+ parameters['Keywords'].should eql 'bar'
81
+ end
82
+
83
+ it 'sets the search index' do
84
+ parameters['SearchIndex'].should eql 'Foo'
85
+ end
86
+ end
87
+
88
+ context 'when given a search index and parameters' do
89
+ before do
90
+ request.search(:foo, :bar => 'baz')
91
+ end
92
+
93
+ it 'sets the parameters' do
94
+ parameters['Bar'].should eql 'baz'
95
+ end
96
+
97
+ it 'sets the search index' do
98
+ parameters['SearchIndex'].should eql 'Foo'
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ module Request
5
+ module Signature
6
+ describe Authentication do
7
+ let(:middleware) do
8
+ described_class.new lambda { |env| env }, 'secret'
9
+ end
10
+
11
+ def result
12
+ env = { :url => 'http:://example.com/foo?Baz=2&Bar=1' }
13
+ middleware.call env
14
+ end
15
+
16
+ it 'sorts the query values' do
17
+ result[:url].query.should match /^Bar/
18
+ end
19
+
20
+ it 'timestamps the request' do
21
+ result[:url].query.should include 'Timestamp'
22
+ end
23
+
24
+ it 'signs the request' do
25
+ result[:url].query.should include 'Signature'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end