dynamodb 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,48 @@
1
+ DynamoDB
2
+ ========
3
+
4
+ Communicate with [Amazon DynamoDB](http://aws.amazon.com/dynamodb/) from Ruby.
5
+
6
+ This is a lightweight alternative to the official Amazon AWS gem.
7
+
8
+ The goal of this library is to be a high performance driver, not an ORM.
9
+
10
+ Usage
11
+ -----
12
+
13
+ The API maps the DynamoDB API closely. Once the connection object is ready, all
14
+ requests are done through `#post`. The first argument is the name of the
15
+ operation, the second is a hash that will be converted to JSON and used as the
16
+ request body.
17
+
18
+ require 'dynamodb'
19
+
20
+ conn = DynamoDB::Connection.new 'ACCESS_KEY', 'SECRET_KEY'
21
+
22
+ conn.post :ListTables
23
+ => {"TableNames" => ["someTable", "anotherTable"]}
24
+
25
+ conn.post :GetItem, {:TableName => "someTable", :Key => {:S => "someKey"}}
26
+ => { ... }
27
+
28
+ TODO
29
+ ----
30
+
31
+ * Implement Signature Version 4
32
+
33
+ Credits
34
+ -------
35
+
36
+ This project started as a fork of [Jedlik](https://github.com/hashmal/jedlik)
37
+ by [Jérémy Pinat](https://github.com/hashmal) but has significantly diverged.
38
+
39
+ License
40
+ -------
41
+
42
+ Copyright (c) 2011-2012 GroupMe, Inc.
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
45
+
46
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+ require "rspec/core/rake_task"
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'dynamodb'
3
+ s.version = '0.0.2'
4
+ s.summary = "Communicate with Amazon DynamoDB."
5
+ s.description = "Communicate with Amazon DynamoDB. Raw access to the full API without having to handle temporary credentials or HTTP requests by yourself."
6
+ s.authors = ["Brandon Keene", "Dave Yeu"]
7
+ s.email = ["bkeene@gmail.com", "dave@groupme.com"]
8
+ s.require_path = 'lib'
9
+ s.files = `git ls-files`.split("\n")
10
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
11
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ s.homepage = 'http://github.com/groupme/dynamodb'
13
+
14
+ s.add_runtime_dependency 'typhoeus', '0.4.2'
15
+ s.add_runtime_dependency 'multi_json', '1.3.7'
16
+
17
+ s.add_development_dependency 'rspec', '2.8.0'
18
+ s.add_development_dependency 'webmock', '1.8.11'
19
+ end
20
+
@@ -0,0 +1,89 @@
1
+ require 'typhoeus'
2
+ require 'time'
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'cgi'
6
+ require 'multi_json'
7
+
8
+ module DynamoDB
9
+ class BaseError < RuntimeError
10
+ attr_reader :response
11
+
12
+ def initialize(response)
13
+ @response = response
14
+ super("#{response.code}: #{response.body}")
15
+ end
16
+ end
17
+
18
+ class ClientError < BaseError; end
19
+ class ServerError < BaseError; end
20
+ class TimeoutError < BaseError; end
21
+ class AuthenticationError < BaseError; end
22
+
23
+ require 'dynamodb/typhoeus/request'
24
+ require 'dynamodb/credentials'
25
+ require 'dynamodb/security_token_service'
26
+ require 'dynamodb/connection'
27
+ require 'dynamodb/response'
28
+
29
+ class << self
30
+ def serialize(object)
31
+ if object.kind_of?(Hash)
32
+ serialized = {}
33
+ object.each do |k, v|
34
+ next if blank?(v)
35
+ serialized[k.to_s] = encode_type(v)
36
+ end
37
+ serialized
38
+ else
39
+ encode_type(object)
40
+ end
41
+ end
42
+
43
+ def deserialize(object)
44
+ if object.values.first.kind_of?(Hash)
45
+ deserialized = {}
46
+ object.each do |k, value_hash|
47
+ deserialized[k] = decode_type(value_hash)
48
+ end
49
+ deserialized
50
+ else
51
+ decode_type(object)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def blank?(object)
58
+ if object.respond_to?(:empty?)
59
+ object.empty?
60
+ else
61
+ object.nil?
62
+ end
63
+ end
64
+
65
+ def encode_type(value)
66
+ case value
67
+ when Numeric
68
+ {"N" => value.to_s}
69
+ when TrueClass, FalseClass
70
+ {"N" => (value ? 1 : 0).to_s}
71
+ when Time
72
+ {"N" => value.to_f.to_s}
73
+ else
74
+ {"S" => value.to_s}
75
+ end
76
+ end
77
+
78
+ def decode_type(value_hash)
79
+ k = value_hash.keys.first
80
+ v = value_hash.values.first
81
+ case k
82
+ when "N" then v.include?('.') ? v.to_f : v.to_i
83
+ when "S" then v.to_s
84
+ else
85
+ raise "Type not recoginized: #{k}"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,97 @@
1
+ module DynamoDB
2
+ # Establishes a connection to Amazon DynamoDB using credentials.
3
+ class Connection
4
+ DEFAULTS = {
5
+ :endpoint => 'dynamodb.us-east-1.amazonaws.com',
6
+ :timeout => 5000 # ms
7
+ }
8
+
9
+ # Acceptable `opts` keys are:
10
+ #
11
+ # :endpoint # DynamoDB endpoint to use.
12
+ # # Default: 'dynamodb.us-east-1.amazonaws.com'
13
+ #
14
+ def initialize(opts = {})
15
+ opts = DEFAULTS.merge opts
16
+
17
+ if opts[:token_service]
18
+ @sts = opts[:token_service]
19
+ elsif opts[:access_key_id] && opts[:secret_access_key]
20
+ @sts = SecurityTokenService.new(opts[:access_key_id], opts[:secret_access_key])
21
+ else
22
+ raise ArgumentError.new("access_key_id and secret_access_key are required")
23
+ end
24
+
25
+ @endpoint = opts[:endpoint]
26
+ @timeout = opts[:timeout]
27
+ end
28
+
29
+ # Create and send a request to DynamoDB.
30
+ # Returns a hash extracted from the response body.
31
+ #
32
+ # `operation` can be any DynamoDB operation. `data` is a hash that will be
33
+ # used as the request body (in JSON format). More info available at:
34
+ #
35
+ # http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide
36
+ #
37
+ def post(operation, data={})
38
+ credentials = @sts.credentials
39
+
40
+ request = new_request(credentials, operation, MultiJson.dump(data))
41
+ request.sign(credentials)
42
+
43
+ hydra.queue(request)
44
+ hydra.run
45
+ response = request.response
46
+
47
+ if response.success?
48
+ case operation
49
+ when :Query, :Scan, :GetItem
50
+ DynamoDB::Response.new(response)
51
+ else
52
+ MultiJson.load(response.body)
53
+ end
54
+ else
55
+ raise_error(response)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def hydra
62
+ Typhoeus::Hydra.hydra
63
+ end
64
+
65
+ def new_request(credentials, operation, body)
66
+ Typhoeus::Request.new "https://#@endpoint/",
67
+ :method => :post,
68
+ :body => body,
69
+ :timeout => @timeout,
70
+ :connect_timeout => @timeout,
71
+ :headers => {
72
+ 'host' => @endpoint,
73
+ 'content-type' => "application/x-amz-json-1.0",
74
+ 'x-amz-date' => (Time.now.utc.strftime "%a, %d %b %Y %H:%M:%S GMT"),
75
+ 'x-amz-security-token' => credentials.session_token,
76
+ 'x-amz-target' => "DynamoDB_20111205.#{operation}",
77
+ }
78
+ end
79
+
80
+ def raise_error(response)
81
+ if response.timed_out?
82
+ raise TimeoutError.new(response)
83
+ else
84
+ case response.code
85
+ when 400..499
86
+ raise ClientError.new(response)
87
+ when 500..599
88
+ raise ServerError.new(response)
89
+ when 0
90
+ raise ServerError.new(response)
91
+ else
92
+ raise BaseError.new(response)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,30 @@
1
+ module DynamoDB
2
+ class Credentials
3
+ attr_reader :access_key_id, :secret_access_key, :session_token
4
+
5
+ def self.from_hash(hash)
6
+ new(hash["access_key_id"], hash["secret_access_key"], hash["session_token"])
7
+ end
8
+
9
+ def initialize(access_key_id, secret_access_key, session_token)
10
+ @access_key_id = access_key_id
11
+ @secret_access_key = secret_access_key
12
+ @session_token = session_token
13
+ end
14
+
15
+ def to_hash
16
+ {
17
+ "access_key_id" => access_key_id,
18
+ "secret_access_key" => secret_access_key,
19
+ "session_token" => session_token
20
+ }
21
+ end
22
+
23
+ def ==(other)
24
+ self.class == other.class &&
25
+ access_key_id == other.access_key_id &&
26
+ secret_access_key == other.secret_access_key &&
27
+ session_token == other.session_token
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ module DynamoDB
2
+ class Response
3
+ attr_reader :typhoeus_response
4
+
5
+ def initialize(typhoeus_response)
6
+ @typhoeus_response = typhoeus_response
7
+ end
8
+
9
+ def hash_key_element
10
+ DynamoDB.deserialize(json["LastEvaluatedKey"]["HashKeyElement"])
11
+ end
12
+
13
+ def range_key_element
14
+ DynamoDB.deserialize(json["LastEvaluatedKey"]["RangeKeyElement"])
15
+ end
16
+
17
+ def item
18
+ return unless json["Item"]
19
+ @item ||= DynamoDB.deserialize(json["Item"])
20
+ end
21
+
22
+ def items
23
+ return unless json["Items"]
24
+ @items ||= json["Items"].map { |i| DynamoDB.deserialize(i) }
25
+ end
26
+
27
+ private
28
+
29
+ def json
30
+ @json ||= MultiJson.load(typhoeus_response.body)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,110 @@
1
+ module DynamoDB
2
+ attr_writer :access_key_id
3
+ attr_writer :secret_acces_key
4
+ attr_writer :session_token
5
+
6
+ # SecurityTokenService automatically manages the creation and renewal of
7
+ # temporary AWS credentials.
8
+ #
9
+ # Usage:
10
+ #
11
+ # credentials = SecurityTokenService.new "id", "secret key"
12
+ # credentials.access_key_id # => String
13
+ # credentials.secret_access_key # => String
14
+ # credentials.session_token # => String
15
+ #
16
+ class SecurityTokenService
17
+ THIRTY_SIX_HOURS = 129600
18
+
19
+ # A SecurityTokenService is initialized for a single AWS user using his
20
+ # credentials.
21
+ def initialize(access_key_id, secret_access_key)
22
+ @_access_key_id = access_key_id
23
+ @_secret_access_key = secret_access_key
24
+ @credentials = nil
25
+ end
26
+
27
+ def credentials
28
+ obtain_credentials
29
+ @credentials
30
+ end
31
+
32
+ private
33
+
34
+ def signature(authorization_params)
35
+ sign(string_to_sign(authorization_params))
36
+ end
37
+
38
+ # The last line needs to be a query string of all parameters
39
+ # in the request in alphabetical order.
40
+ def string_to_sign(authorization_params)
41
+ [
42
+ "GET",
43
+ "sts.amazonaws.com",
44
+ "/",
45
+ "AWSAccessKeyId=#{@_access_key_id}" +
46
+ "&Action=GetSessionToken" +
47
+ "&DurationSeconds=#{THIRTY_SIX_HOURS}" +
48
+ "&SignatureMethod=HmacSHA256" +
49
+ "&SignatureVersion=2" +
50
+ "&Timestamp=#{CGI.escape(authorization_params[:Timestamp])}" +
51
+ "&Version=2011-06-15"
52
+ ].join("\n")
53
+ end
54
+
55
+ # Extract the contents of a given tag.
56
+ def get_tag(tag, string)
57
+ # Considering that the XML string received from STS is sane and always
58
+ # has the same simple structure, I think a simple regular expression
59
+ # can do the job (with the benefit of not adding a dependency on
60
+ # another library just for ONE method). I will switch to Nokogiri if
61
+ # needed.
62
+ string.match(/#{tag.to_s}>([^<]*)/)[1]
63
+ end
64
+
65
+ # Obtain temporary credentials, set to expire after 1 hour. If
66
+ # credentials were previously obtained, no request is made until they
67
+ # expire.
68
+ def obtain_credentials
69
+ return unless credentials_expired?
70
+
71
+ authorization_params = {
72
+ :Action => 'GetSessionToken',
73
+ :Timestamp => Time.now.utc.iso8601,
74
+ :Version => '2011-06-15',
75
+ :DurationSeconds => THIRTY_SIX_HOURS # 36 hour expiration
76
+ }
77
+
78
+ params = {
79
+ :AWSAccessKeyId => @_access_key_id,
80
+ :SignatureMethod => 'HmacSHA256',
81
+ :SignatureVersion => '2',
82
+ :Signature => signature(authorization_params)
83
+ }.merge(authorization_params)
84
+
85
+ response = Typhoeus::Request.get("https://sts.amazonaws.com", :params => params)
86
+ if response.success?
87
+ body = response.body
88
+ @expiration = Time.parse(get_tag(:Expiration, body))
89
+ @credentials = Credentials.new(
90
+ get_tag(:AccessKeyId, body),
91
+ get_tag(:SecretAccessKey, body),
92
+ get_tag(:SessionToken, body))
93
+ else
94
+ raise AuthenticationError.new(response)
95
+ end
96
+ end
97
+
98
+ # Sign (HMAC-SHA256) a string using the secret key given at
99
+ # initialization.
100
+ def sign(string)
101
+ Base64.encode64(
102
+ OpenSSL::HMAC.digest('sha256', @_secret_access_key, string)
103
+ ).strip
104
+ end
105
+
106
+ def credentials_expired?
107
+ @expiration.nil? || @expiration <= Time.now.utc
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,27 @@
1
+ module Typhoeus
2
+ class Request
3
+ def sign(credentials)
4
+ headers.merge!('x-amzn-authorization' => "AWS3 AWSAccessKeyId=#{credentials.access_key_id},Algorithm=HmacSHA256,Signature=#{digest(credentials.secret_access_key)}")
5
+ end
6
+
7
+ private
8
+
9
+ def digest(secret_key)
10
+ Base64.encode64(
11
+ OpenSSL::HMAC.digest('sha256', secret_key, Digest::SHA256.digest(string_to_sign))
12
+ ).strip
13
+ end
14
+
15
+ def string_to_sign
16
+ "POST\n/\n\nhost:#{parsed_uri.host}\n#{amz_to_sts}\n#{body}"
17
+ end
18
+
19
+ def amz_to_sts
20
+ get_amz_headers.sort.map {|key, val| [key, val].join(':') + "\n"}.join
21
+ end
22
+
23
+ def get_amz_headers
24
+ headers.select {|key, val| key =~ /\Ax-amz-/}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+ require 'benchmark'
3
+
4
+ describe DynamoDB::Connection do
5
+ let(:token_service) {
6
+ stub(:credentials =>
7
+ DynamoDB::Credentials.new(
8
+ "access_key_id",
9
+ "secret_access_key",
10
+ "session_token"
11
+ )
12
+ )
13
+ }
14
+
15
+ describe "#initialize" do
16
+ it "can be initialized with a token service" do
17
+ DynamoDB::Connection.new(:token_service => token_service)
18
+ end
19
+
20
+ it "can be initialized with an access key" do
21
+ DynamoDB::Connection.new(
22
+ :access_key_id => "id",
23
+ :secret_access_key => "secret"
24
+ )
25
+ end
26
+
27
+ context "no token service was provided" do
28
+ it "requires an access_key_id and secret_access_key" do
29
+ lambda {
30
+ DynamoDB::Connection.new
31
+ }.should raise_error(ArgumentError)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "#post" do
37
+ let(:connection) { DynamoDB::Connection.new(:token_service => token_service) }
38
+
39
+ before do
40
+ Time.stub(:now => Time.at(1332635893)) # Sat Mar 24 20:38:13 -0400 2012
41
+
42
+ @url = "https://dynamodb.us-east-1.amazonaws.com/"
43
+ @headers = {
44
+ 'Content-Type' => 'application/x-amz-json-1.0',
45
+ 'Host' => 'dynamodb.us-east-1.amazonaws.com',
46
+ 'X-Amz-Date' => 'Sun, 25 Mar 2012 00:38:13 GMT',
47
+ 'X-Amz-Security-Token' => 'session_token',
48
+ 'X-Amzn-Authorization' => 'AWS3 AWSAccessKeyId=access_key_id,Algorithm=HmacSHA256,Signature=2xa6v0WW+980q8Hgt+ym3/7C0D1DlkueGMugi1NWE+o='
49
+ }
50
+ end
51
+
52
+ it "signs and posts a request" do
53
+ @headers['X-Amz-Target'] = 'DynamoDB_20111205.ListTables'
54
+ stub_request(:post, @url).
55
+ with(
56
+ :body => "{}",
57
+ :headers => @headers
58
+ ).
59
+ to_return(
60
+ :status => 200,
61
+ :body => '{"TableNames":["example"]}',
62
+ :headers => {}
63
+ )
64
+
65
+ result = connection.post :ListTables
66
+ result.should == {"TableNames" => ["example"]}
67
+ end
68
+
69
+ it "type casts response when Query" do
70
+ stub_request(:post, @url).
71
+ to_return(
72
+ :status => 200,
73
+ :body => "{}",
74
+ :headers => {}
75
+ )
76
+
77
+ response = connection.post :Query, :TableName => "people", :HashKeyId => {:N => "1"}
78
+ response.should be_a_kind_of(DynamoDB::Response)
79
+ end
80
+
81
+ it "type casts response when GetItem" do
82
+ stub_request(:post, @url).
83
+ to_return(
84
+ :status => 200,
85
+ :body => "{}",
86
+ :headers => {}
87
+ )
88
+
89
+ response = connection.post :GetItem, :TableName => "people", :Key => {:HashKeyElement => {:N => "1"}, :RangeKeyElement => {:N => 2}}
90
+ response.should be_a_kind_of(DynamoDB::Response)
91
+ end
92
+
93
+ context "when a failure occurs" do
94
+ it "raises an error with the response attached" do
95
+ stub_request(:post, @url).to_return(:status => 500, :body => "Failed for some reason.")
96
+ error = nil
97
+ begin
98
+ connection.post :Query, :TableName => "people", :HashKeyId => {:N => "1"}
99
+ rescue => e
100
+ error = e
101
+ end
102
+ error.should be_an_instance_of(DynamoDB::ServerError)
103
+ error.response.code.should == 500
104
+ error.message.should == "500: Failed for some reason."
105
+ end
106
+ end
107
+
108
+ context "when the connection fails" do
109
+ it "raises a server error" do
110
+ stub_request(:post, @url).to_return(:status => 0, :body => "")
111
+ error = nil
112
+ begin
113
+ connection.post :Query, :TableName => "people", :HashKeyId => {:N => "1"}
114
+ rescue => e
115
+ error = e
116
+ end
117
+ error.should be_an_instance_of(DynamoDB::ServerError)
118
+ error.response.code.should == 0
119
+ end
120
+ end
121
+
122
+ context "when the connection times out" do
123
+ it "raises a DynamoDB::TimeoutError" do
124
+ stub_request(:post, @url).to_timeout
125
+ expect {
126
+ connection.post :Query, :TableName => "people", :HashKeyId => {:N => "1"}
127
+ }.to raise_error(DynamoDB::TimeoutError)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe DynamoDB::Credentials do
4
+ describe "#==" do
5
+ it "is true for identical credentials" do
6
+ DynamoDB::Credentials.new("abc", "123", "token").should ==
7
+ DynamoDB::Credentials.new("abc", "123", "token")
8
+ end
9
+
10
+ it "is false otherwise" do
11
+ DynamoDB::Credentials.new("abc", "123", "token").should_not ==
12
+ DynamoDB::Credentials.new("abc", "123", "different token")
13
+ end
14
+ end
15
+
16
+ describe "#to_hash" do
17
+ it "converts to a hash and back" do
18
+ credentials = DynamoDB::Credentials.new("abc", "123", "token")
19
+ DynamoDB::Credentials.from_hash(credentials.to_hash).should == credentials
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe DynamoDB::Response do
4
+ before do
5
+ body = {
6
+ "LastEvaluatedKey" => {
7
+ "HashKeyElement" => {"N"=>"1"},
8
+ "RangeKeyElement"=>{"N"=>"1501"}
9
+ },
10
+ "Items"=>[
11
+ {
12
+ "name"=>{"S"=>"John Smith"},
13
+ "created_at"=>{"N"=>"1321564309.99428"},
14
+ "disabled"=>{"N"=>"0"},
15
+ "group_id"=>{"N"=>"1"},
16
+ "person_id"=>{"N" => "1500"}
17
+ },
18
+ {
19
+ "name"=>{"S"=>"Jane Smith"},
20
+ "created_at"=>{"N"=>"1321564309.99428"},
21
+ "disabled"=>{"N"=>"1"},
22
+ "group_id"=>{"N"=>"1"},
23
+ "person_id"=>{"N" => "1501"}
24
+ }
25
+ ],
26
+ "Count" => 1,
27
+ "ConsumedCapacityUnits" => 0.5
28
+ }
29
+ @typhoeus_response = mock("response", :body => MultiJson.dump(body))
30
+ end
31
+
32
+ describe "#hash_key_element" do
33
+ it "returns the typecast value of HashKeyElement" do
34
+ response = DynamoDB::Response.new(@typhoeus_response)
35
+ response.hash_key_element.should == 1
36
+ end
37
+ end
38
+
39
+ describe "#range_key_element" do
40
+ it "returns the typecast value of RangeKeyElement" do
41
+ response = DynamoDB::Response.new(@typhoeus_response)
42
+ response.range_key_element.should == 1501
43
+ end
44
+ end
45
+
46
+ context "Items" do
47
+ it "type casts response" do
48
+ response = DynamoDB::Response.new(@typhoeus_response)
49
+ response.items[0]["name"].should == "John Smith"
50
+ response.items[0]["created_at"].should == 1321564309.99428
51
+ response.items[0]["disabled"].should == 0
52
+ response.items[0]["group_id"].should == 1
53
+ response.items[0]["person_id"].should == 1500
54
+
55
+ response.items[1]["name"].should == "Jane Smith"
56
+ response.items[1]["created_at"].should == 1321564309.99428
57
+ response.items[1]["disabled"].should == 1
58
+ response.items[1]["group_id"].should == 1
59
+ response.items[1]["person_id"].should == 1501
60
+ end
61
+ end
62
+
63
+ context "Item" do
64
+ before do
65
+ body = {
66
+ "Item"=> {
67
+ "name"=>{"S"=>"John Smith"},
68
+ "created_at"=>{"N"=>"1321564309.99428"},
69
+ "disabled"=>{"N"=>"0"},
70
+ "group_id"=>{"N"=>"1"},
71
+ "person_id"=>{"N" => "1500"}
72
+ },
73
+ "ConsumedCapacityUnits" => 0.5
74
+ }
75
+ @typhoeus_response = mock("response", :body => MultiJson.dump(body))
76
+ end
77
+
78
+ it "type casts response" do
79
+ response = DynamoDB::Response.new(@typhoeus_response)
80
+ response.item["name"].should == "John Smith"
81
+ response.item["created_at"].should == 1321564309.99428
82
+ response.item["disabled"].should == 0
83
+ response.item["group_id"].should == 1
84
+ response.item["person_id"].should == 1500
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module DynamoDB
4
+ describe SecurityTokenService do
5
+ let(:sts) { SecurityTokenService.new("access_key_id", "secret_access_key") }
6
+
7
+ context "success" do
8
+ before do
9
+ Time.stub(:now).and_return(Time.parse("2012-03-24T22:10:38Z"))
10
+ success_body = <<-XML
11
+ <GetSessionTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
12
+ <GetSessionTokenResult>
13
+ <Credentials>
14
+ <SessionToken>session_token</SessionToken>
15
+ <SecretAccessKey>secret_access_key</SecretAccessKey>
16
+ <Expiration>2036-03-19T01:03:22.276Z</Expiration>
17
+ <AccessKeyId>access_key_id</AccessKeyId>
18
+ </Credentials>
19
+ </GetSessionTokenResult>
20
+ <ResponseMetadata>
21
+ <RequestId>f0fa5827-7156-11e1-8f1e-a92b58fdc66e</RequestId>
22
+ </ResponseMetadata>
23
+ </GetSessionTokenResponse>
24
+ XML
25
+
26
+ @request = stub_request(:get, "https://sts.amazonaws.com/").
27
+ with(:query => {
28
+ "AWSAccessKeyId" => "access_key_id",
29
+ "Action" => "GetSessionToken",
30
+ "Signature" => "HyF65paDxprCe+zEHx7wxah+hiZ43PhvtmeehDtfuw8=",
31
+ "SignatureMethod" => "HmacSHA256",
32
+ "SignatureVersion" => "2",
33
+ "Timestamp" => "2012-03-24T22:10:38Z",
34
+ "Version" => "2011-06-15",
35
+ "DurationSeconds" => "129600"
36
+ }).to_return(:status => 200, :body => success_body)
37
+ end
38
+
39
+ it "obtains session_token, access_key_id, secret_access_key" do
40
+ sts = SecurityTokenService.new("access_key_id", "secret_access_key")
41
+ credentials = sts.credentials
42
+ credentials.access_key_id.should == "access_key_id"
43
+ credentials.secret_access_key.should == "secret_access_key"
44
+ credentials.session_token.should == "session_token"
45
+ end
46
+
47
+ it "does not query STS if the credentials are not expired" do
48
+ sts = SecurityTokenService.new("access_key_id", "secret_access_key")
49
+
50
+ sts.stub(:credentials_expired?).and_return(true)
51
+ sts.credentials
52
+ @request.should have_been_requested
53
+
54
+ WebMock.reset!
55
+
56
+ sts.stub(:credentials_expired?).and_return(false)
57
+ sts.credentials
58
+ @request.should_not have_been_requested
59
+ end
60
+ end
61
+
62
+ context "failure" do
63
+ before do
64
+ Time.stub(:now).and_return(Time.parse("2012-03-24T22:10:38Z"))
65
+ error_body = <<-XML
66
+ <ErrorResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
67
+ <Error>
68
+ <Type>Sender</Type>
69
+ <Code>InvalidClientTokenId</Code>
70
+ <Message>The security token included in the request is invalid</Message>
71
+ </Error>
72
+ <RequestId>a9f51cd0-7f5b-11e1-8022-bd0f2fc51c4b</RequestId>
73
+ </ErrorResponse>
74
+ XML
75
+
76
+ stub_request(:get, "https://sts.amazonaws.com/").
77
+ with(:query => {
78
+ "AWSAccessKeyId" => "access_key_id",
79
+ "Action" => "GetSessionToken",
80
+ "Signature" => "HyF65paDxprCe+zEHx7wxah+hiZ43PhvtmeehDtfuw8=",
81
+ "SignatureMethod" => "HmacSHA256",
82
+ "SignatureVersion" => "2",
83
+ "Timestamp" => "2012-03-24T22:10:38Z",
84
+ "Version" => "2011-06-15",
85
+ "DurationSeconds" => "129600"
86
+ }).to_return(:status => 403, :body => error_body)
87
+ end
88
+
89
+ it "raises an AuthenticationError" do
90
+ s = SecurityTokenService.new("access_key_id", "secret_access_key")
91
+ proc {
92
+ s.credentials
93
+ }.should raise_error(DynamoDB::AuthenticationError)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe DynamoDB do
4
+ describe "#serialize" do
5
+ it "serializes to dynamo type format" do
6
+ published_at = Time.now
7
+ hash = {
8
+ :id => 1,
9
+ :name => "Gone with the Wind",
10
+ :published_at => published_at,
11
+ :price => 11.99,
12
+ :active => true
13
+ }
14
+
15
+ DynamoDB.serialize(hash).should == {
16
+ "id" => {"N" => "1"},
17
+ "name" => {"S" => "Gone with the Wind"},
18
+ "published_at" => {"N" => published_at.to_f.to_s},
19
+ "price" => {"N" => "11.99"},
20
+ "active" => {"N" => "1"}
21
+ }
22
+ end
23
+
24
+ it "serializes a single value" do
25
+ DynamoDB.serialize(1).should == {"N" => "1"}
26
+ DynamoDB.serialize(1.5).should == {"N" => "1.5"}
27
+ DynamoDB.serialize("Hello World").should == {"S" => "Hello World"}
28
+ end
29
+
30
+ it "omits nil/blank values" do
31
+ DynamoDB.serialize("foo" => nil).should == {}
32
+ DynamoDB.serialize("foo" => "").should == {}
33
+ end
34
+ end
35
+
36
+ describe "#deserialize" do
37
+ it "deserializes single values" do
38
+ DynamoDB.deserialize({"N" => "123"}).should == 123
39
+ DynamoDB.deserialize({"S" => "Hello World"}).should == "Hello World"
40
+ end
41
+
42
+ it "deserializes from dynamo type format" do
43
+ published_at = Time.now
44
+ item = {
45
+ "id" => {"N" => "1"},
46
+ "name" => {"S" => "Gone with the Wind"},
47
+ "published_at" => {"N" => published_at.to_f.to_s},
48
+ "price" => {"N" => "11.99"},
49
+ "active" => {"N" => "1"}
50
+ }
51
+ deserialized = DynamoDB.deserialize(item)
52
+ deserialized["id"].should == 1
53
+ deserialized["name"].should == "Gone with the Wind"
54
+ deserialized["published_at"].to_s.should == published_at.to_f.to_s
55
+ deserialized["price"].should == 11.99
56
+ deserialized["active"].should == 1
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+
3
+ $:.unshift(File.join(File.dirname __FILE__), 'lib')
4
+ require 'dynamodb'
5
+ require 'webmock/rspec'
6
+
7
+ RSpec.configure do |config|
8
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamodb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brandon Keene
9
+ - Dave Yeu
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-11-15 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: typhoeus
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - '='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.4.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - '='
29
+ - !ruby/object:Gem::Version
30
+ version: 0.4.2
31
+ - !ruby/object:Gem::Dependency
32
+ name: multi_json
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - '='
37
+ - !ruby/object:Gem::Version
38
+ version: 1.3.7
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.7
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 2.8.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - '='
61
+ - !ruby/object:Gem::Version
62
+ version: 2.8.0
63
+ - !ruby/object:Gem::Dependency
64
+ name: webmock
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - '='
69
+ - !ruby/object:Gem::Version
70
+ version: 1.8.11
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - '='
77
+ - !ruby/object:Gem::Version
78
+ version: 1.8.11
79
+ description: Communicate with Amazon DynamoDB. Raw access to the full API without
80
+ having to handle temporary credentials or HTTP requests by yourself.
81
+ email:
82
+ - bkeene@gmail.com
83
+ - dave@groupme.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - .gitignore
89
+ - .rspec
90
+ - Gemfile
91
+ - README.md
92
+ - Rakefile
93
+ - dynamodb.gemspec
94
+ - lib/dynamodb.rb
95
+ - lib/dynamodb/connection.rb
96
+ - lib/dynamodb/credentials.rb
97
+ - lib/dynamodb/response.rb
98
+ - lib/dynamodb/security_token_service.rb
99
+ - lib/dynamodb/typhoeus/request.rb
100
+ - spec/dynamodb/connection_spec.rb
101
+ - spec/dynamodb/credentials_spec.rb
102
+ - spec/dynamodb/response_spec.rb
103
+ - spec/dynamodb/security_token_service_spec.rb
104
+ - spec/dynamodb_spec.rb
105
+ - spec/spec_helper.rb
106
+ homepage: http://github.com/groupme/dynamodb
107
+ licenses: []
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.23
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: Communicate with Amazon DynamoDB.
130
+ test_files:
131
+ - spec/dynamodb/connection_spec.rb
132
+ - spec/dynamodb/credentials_spec.rb
133
+ - spec/dynamodb/response_spec.rb
134
+ - spec/dynamodb/security_token_service_spec.rb
135
+ - spec/dynamodb_spec.rb
136
+ - spec/spec_helper.rb