simplydb 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +37 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/examples/interface.rb +19 -0
- data/examples/record.rb +38 -0
- data/lib/simplydb/client.rb +66 -0
- data/lib/simplydb/clients/typhoeus.rb +35 -0
- data/lib/simplydb/error.rb +60 -0
- data/lib/simplydb/extensions.rb +21 -0
- data/lib/simplydb/interface.rb +207 -0
- data/lib/simplydb/record/base.rb +253 -0
- data/lib/simplydb.rb +5 -0
- data/simplydb.gemspec +82 -0
- data/spec/client_spec.rb +55 -0
- data/spec/error_spec.rb +9 -0
- data/spec/extensions_spec.rb +15 -0
- data/spec/interface_spec.rb +223 -0
- data/spec/record/base_spec.rb +198 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +24 -0
- metadata +160 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 JT Archie
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
= simplydb
|
2
|
+
|
3
|
+
A minimal interface to Amazon SimpleDB that has separation of interfaces. From the low level HTTP request access to high level Ruby abstraction ORM.
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'simplydb'
|
7
|
+
|
8
|
+
interface = SimplyDB::Interface.new({
|
9
|
+
:access_key => ENV['AWS_ACCESS_KEY'],
|
10
|
+
:secret_key => ENV['AWS_SECRET_KEY']
|
11
|
+
})
|
12
|
+
|
13
|
+
if interface.create_domain("MyDomain")
|
14
|
+
interface.put_attributes('MyDomain', 'Item123', {'color'=>['red','brick','garnet']})
|
15
|
+
|
16
|
+
attributes = interface.get_attributes('MyDomain', 'Item123')
|
17
|
+
puts "Item123 = #{attributes.inspect}"
|
18
|
+
|
19
|
+
items = interface.select("select color from MyDomain where color = 'brick'")
|
20
|
+
puts "Items = #{items.inspect}"
|
21
|
+
|
22
|
+
interface.delete_domain("MyDomain")
|
23
|
+
end
|
24
|
+
|
25
|
+
== Note on Patches/Pull Requests
|
26
|
+
|
27
|
+
* Fork the project.
|
28
|
+
* Make your feature addition or bug fix.
|
29
|
+
* Add tests for it. This is important so I don't break it in a
|
30
|
+
future version unintentionally.
|
31
|
+
* Commit, do not mess with rakefile, version, or history.
|
32
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
33
|
+
* Send me a pull request. Bonus points for topic branches.
|
34
|
+
|
35
|
+
== Copyright
|
36
|
+
|
37
|
+
Copyright (c) 2010 JT Archie. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "simplydb"
|
8
|
+
gem.summary = %Q{A minimal interface to Amazon SimpleDB.}
|
9
|
+
gem.description = %Q{A minimal interface to Amazon SimpleDB that has separation of interfaces. From the low level HTTP request access to high level Ruby abstraction ORM.}
|
10
|
+
gem.email = "jtarchie@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/jtarchie/simplydb"
|
12
|
+
gem.authors = ["JT Archie"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_dependency "typhoeus", ">= 0.1.27"
|
15
|
+
gem.add_dependency "nokogiri", ">= 1.4.2"
|
16
|
+
gem.add_dependency "uuidtools", ">= 2.1.1"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
desc "irb loaded with SimplyDB (rubygems not required)"
|
41
|
+
task :irb do
|
42
|
+
exec("irb -I lib/ -r 'simplydb' -r 'simplydb/record/base'")
|
43
|
+
end
|
44
|
+
task :console => :irb
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "simplydb #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'simplydb'
|
3
|
+
|
4
|
+
interface = SimplyDB::Interface.new({
|
5
|
+
:access_key => ENV['AWS_ACCESS_KEY'],
|
6
|
+
:secret_key => ENV['AWS_SECRET_KEY']
|
7
|
+
})
|
8
|
+
|
9
|
+
if interface.create_domain("MyDomain")
|
10
|
+
interface.put_attributes('MyDomain', 'Item123', {'color'=>['red','brick','garnet']})
|
11
|
+
|
12
|
+
attributes = interface.get_attributes('MyDomain', 'Item123')
|
13
|
+
puts "Item123 = #{attributes.inspect}"
|
14
|
+
|
15
|
+
items = interface.select("select color from MyDomain where color = 'brick'")
|
16
|
+
puts "Items = #{items.inspect}"
|
17
|
+
|
18
|
+
interface.delete_domain("MyDomain")
|
19
|
+
end
|
data/examples/record.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'simplydb'
|
3
|
+
require 'simplydb/record/base'
|
4
|
+
|
5
|
+
SimplyDB::Record::Base.establish_connection({
|
6
|
+
'access_key' => ENV['AWS_ACCESS_KEY'],
|
7
|
+
'secret_key' => ENV['AWS_SECRET_KEY']
|
8
|
+
})
|
9
|
+
|
10
|
+
class MyDomain < SimplyDB::Record::Base; end
|
11
|
+
|
12
|
+
MyDomain.create_domain
|
13
|
+
|
14
|
+
puts "domain name: #{MyDomain.domain_name}"
|
15
|
+
puts "domain exists?: #{MyDomain.domain_exists?}"
|
16
|
+
|
17
|
+
model = MyDomain.new(
|
18
|
+
:name => 'JT Archie',
|
19
|
+
:age => 27
|
20
|
+
)
|
21
|
+
puts "attribues: #{model.attributes.inspect}"
|
22
|
+
model[:age] = 29
|
23
|
+
puts "attribues: #{model.attributes.inspect}"
|
24
|
+
|
25
|
+
puts "model.new_record?: #{model.new_record?}"
|
26
|
+
|
27
|
+
model.item_name = "Testing"
|
28
|
+
puts "save: #{model.save}"
|
29
|
+
|
30
|
+
sleep 2 #let SimpleDB propogate data (not supporting consistant read, yet)
|
31
|
+
|
32
|
+
got_model = MyDomain.find('Testing')
|
33
|
+
puts "got_model: #{got_model.attributes.inspect}"
|
34
|
+
|
35
|
+
models = MyDomain.find_by_select('SELECT * FROM my_domain')
|
36
|
+
puts "models: #{models.inspect}"
|
37
|
+
|
38
|
+
#MyDomain.delete_domain
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'base64'
|
4
|
+
require 'uri'
|
5
|
+
require 'simplydb/clients/typhoeus'
|
6
|
+
|
7
|
+
module SimplyDB
|
8
|
+
class Client
|
9
|
+
include SimplyDB::Extensions
|
10
|
+
|
11
|
+
attr_accessor :options, :http_client
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@options = {
|
15
|
+
:async => false,
|
16
|
+
:protocol => 'https://',
|
17
|
+
:host => 'sdb.amazonaws.com',
|
18
|
+
:port => 443,
|
19
|
+
:path => "/",
|
20
|
+
:signature_version => '2',
|
21
|
+
:version => '2009-04-15',
|
22
|
+
:signature_method => 'HmacSHA256',
|
23
|
+
}.merge(options)
|
24
|
+
@http_client = (@options[:client] || SimplyDB::Clients::Typhoeus).new(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def async?
|
28
|
+
return options[:async] == true
|
29
|
+
end
|
30
|
+
|
31
|
+
def base_url
|
32
|
+
"#{@options[:protocol]}#{@options[:host]}:#{@options[:port]}#{@options[:path]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def string_to_sign(method, params)
|
36
|
+
return "#{method.to_s.upcase}\n#{options[:host]}\n/\n" + escape_hash(params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_signature(method, params)
|
40
|
+
Base64.encode64(
|
41
|
+
OpenSSL::HMAC.digest(
|
42
|
+
OpenSSL::Digest::Digest.new('sha256'),
|
43
|
+
@options[:secret_key],
|
44
|
+
string_to_sign(method, params)
|
45
|
+
)
|
46
|
+
).chomp
|
47
|
+
end
|
48
|
+
|
49
|
+
def params_with_signature(method, params)
|
50
|
+
params.merge!({
|
51
|
+
'AWSAccessKeyId' => @options[:access_key],
|
52
|
+
'SignatureVersion' => @options[:signature_version],
|
53
|
+
'Timestamp' => Time.now.iso8601,
|
54
|
+
'Version' => @options[:version],
|
55
|
+
'SignatureMethod' => @options[:signature_method]
|
56
|
+
})
|
57
|
+
params['Signature'] = generate_signature(method, params)
|
58
|
+
return params
|
59
|
+
end
|
60
|
+
|
61
|
+
def call(method, params ={}, &on_complete)
|
62
|
+
options = {:method => method, :url => base_url, :params => params_with_signature(method, params)}
|
63
|
+
@http_client.request(options, !async?) {|request| on_complete.call(request.body)}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
|
3
|
+
module SimplyDB
|
4
|
+
module Clients
|
5
|
+
class Typhoeus
|
6
|
+
|
7
|
+
attr_accessor :hydra, :options
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = options
|
11
|
+
@hydra = ::Typhoeus::Hydra.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def request(options={}, force = true, &block)
|
15
|
+
request = ::Typhoeus::Request.new(options[:url],
|
16
|
+
:method => options[:method],
|
17
|
+
:params => options[:params]
|
18
|
+
)
|
19
|
+
|
20
|
+
@hydra.queue(request)
|
21
|
+
|
22
|
+
unless force
|
23
|
+
request.on_complete {|response| block.call(response)}
|
24
|
+
else
|
25
|
+
@hydra.run
|
26
|
+
block.call(request.response)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def run!
|
31
|
+
@hydra.run
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module SimplyDB
|
2
|
+
module Error
|
3
|
+
# ref http://docs.amazonwebservices.com/AmazonSimpleDB/latest/DeveloperGuide/APIError.html
|
4
|
+
class AccessFailure < RuntimeError; end
|
5
|
+
class AttributeDoesNotExist < RuntimeError; end
|
6
|
+
class AuthFailure < RuntimeError; end
|
7
|
+
class AuthMissingFailure < RuntimeError; end
|
8
|
+
class ConditionalCheckFailed < RuntimeError; end
|
9
|
+
class ExistsAndExpectedValue < RuntimeError; end
|
10
|
+
class FeatureDeprecated < RuntimeError; end
|
11
|
+
class IncompleteExpectedExpression < RuntimeError; end
|
12
|
+
class InternalError < RuntimeError; end
|
13
|
+
class InvalidAction < RuntimeError; end
|
14
|
+
class InvalidHTTPAuthHeader < RuntimeError; end
|
15
|
+
class InvalidHttpRequest < RuntimeError; end
|
16
|
+
class InvalidLiteral < RuntimeError; end
|
17
|
+
class InvalidNextToken < RuntimeError; end
|
18
|
+
class InvalidNumberPredicates < RuntimeError; end
|
19
|
+
class InvalidNumberValueTests < RuntimeError; end
|
20
|
+
class InvalidParameterCombination < RuntimeError; end
|
21
|
+
class InvalidParameterValue < RuntimeError; end
|
22
|
+
class InvalidQueryExpression < RuntimeError; end
|
23
|
+
class InvalidResponseGroups < RuntimeError; end
|
24
|
+
class InvalidService < RuntimeError; end
|
25
|
+
class InvalidSOAPRequest < RuntimeError; end
|
26
|
+
class InvalidSortExpression < RuntimeError; end
|
27
|
+
class InvalidURI < RuntimeError; end
|
28
|
+
class InvalidWSAddressingProperty < RuntimeError; end
|
29
|
+
class InvalidWSDLVersion < RuntimeError; end
|
30
|
+
class MalformedSOAPSignature < RuntimeError; end
|
31
|
+
class MissingAction < RuntimeError; end
|
32
|
+
class MissingParameter < RuntimeError; end
|
33
|
+
class MissingWSAddressingProperty < RuntimeError; end
|
34
|
+
class MultipleExistsConditions < RuntimeError; end
|
35
|
+
class MultipleExpectedNames < RuntimeError; end
|
36
|
+
class MultipleExpectedValues < RuntimeError; end
|
37
|
+
class MultiValuedAttribute < RuntimeError; end
|
38
|
+
class NoSuchDomain < RuntimeError; end
|
39
|
+
class NoSuchVersion < RuntimeError; end
|
40
|
+
class NotYetImplemented < RuntimeError; end
|
41
|
+
class NumberDomainsExceeded < RuntimeError; end
|
42
|
+
class NumberDomainAttributesExceeded < RuntimeError; end
|
43
|
+
class NumberDomainBytesExceeded < RuntimeError; end
|
44
|
+
class NumberItemAttributesExceeded < RuntimeError; end
|
45
|
+
class NumberSubmittedAttributesExceeded < RuntimeError; end
|
46
|
+
class NumberSubmittedItemsExceeded < RuntimeError; end
|
47
|
+
class RequestExpired < RuntimeError; end
|
48
|
+
class RequestTimeout < RuntimeError; end
|
49
|
+
class ServiceUnavailable < RuntimeError; end
|
50
|
+
class TooManyRequestedAttributes < RuntimeError; end
|
51
|
+
class UnsupportedHttpVerb < RuntimeError; end
|
52
|
+
class UnsupportedNextToken < RuntimeError; end
|
53
|
+
class URITooLong < RuntimeError; end
|
54
|
+
|
55
|
+
#Standard AWS errors
|
56
|
+
class SignatureDoesNotMatch < RuntimeError; end
|
57
|
+
class InvalidClientTokenId < RuntimeError; end
|
58
|
+
class InvalidRequest < RuntimeError; end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SimplyDB
|
2
|
+
module Extensions
|
3
|
+
def underscore(camel_cased_word)
|
4
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
5
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
6
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
7
|
+
tr("-", "_").
|
8
|
+
downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def escape_value(string)
|
12
|
+
string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
|
13
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
14
|
+
end.gsub(' ', '%20')
|
15
|
+
end
|
16
|
+
|
17
|
+
def escape_hash(params = {})
|
18
|
+
return params.collect{|k,v| [k.to_s, v.to_s]}.sort.collect { |key, value| [escape_value(key), escape_value(value)].join('=') }.join('&')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module SimplyDB
|
4
|
+
class Interface
|
5
|
+
attr_accessor :options, :client, :request_id, :box_usage, :next_token, :on_error
|
6
|
+
def initialize(options = {})
|
7
|
+
@options = {}.merge(options)
|
8
|
+
@client = SimplyDB::Client.new(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_domain(name, &block)
|
12
|
+
call({'Action' => 'CreateDomain', 'DomainName' => name}) do |doc|
|
13
|
+
ret_val = doc.css("CreateDomainResponse").length > 0
|
14
|
+
block.call(ret_val) if block_given?
|
15
|
+
ret_val
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete_domain(name, &block)
|
20
|
+
call({'Action' => 'DeleteDomain', 'DomainName' => name}) do |doc|
|
21
|
+
ret_val = doc.css("DeleteDomainResponse").length > 0
|
22
|
+
block.call(ret_val) if block_given?
|
23
|
+
ret_val
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def list_domains(&block)
|
28
|
+
call({'Action' => 'ListDomains'}) do |doc|
|
29
|
+
domains = doc.css('DomainName').collect{|d| d.text}
|
30
|
+
block.call(domains) if block_given?
|
31
|
+
domains
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def domain_metadata(name, &block)
|
36
|
+
call({'Action' => 'DomainMetadata','DomainName' => name}) do |doc|
|
37
|
+
attributes = doc.css("DomainMetadataResult").first.children.inject({}) do |memo, child|
|
38
|
+
memo[child.name] = child.text
|
39
|
+
memo
|
40
|
+
end
|
41
|
+
block.call(attributes) if block_given?
|
42
|
+
attributes
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def put_attributes(name, id, attributes = {}, expected = {}, replace = false, &block)
|
47
|
+
params = define_attributes(attributes, expected, replace)
|
48
|
+
params.merge!({
|
49
|
+
'DomainName' => name,
|
50
|
+
'ItemName' => id,
|
51
|
+
'Action' => 'PutAttributes'
|
52
|
+
})
|
53
|
+
call(params) do |doc|
|
54
|
+
ret_val = doc.css("PutAttributesResponse").length > 0
|
55
|
+
block.call(ret_val) if block_given?
|
56
|
+
ret_val
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def batch_put_attributes(name, items = {}, &block)
|
61
|
+
params = {'DomainName' => name, 'ActionName' => 'BatchPutAttributes'}
|
62
|
+
items.keys.each_with_index do |key, i|
|
63
|
+
params["Item.#{i}.ItemName"] = key
|
64
|
+
items[key].inject(0) do |j, (name,value)|
|
65
|
+
value = [value] unless value.is_a?(Array)
|
66
|
+
value.each do |v|
|
67
|
+
params["Item.#{i}.Attribute.#{j}.Name"] = name
|
68
|
+
params["Item.#{i}.Attribute.#{j}.Value"] = value
|
69
|
+
j+=1
|
70
|
+
end
|
71
|
+
j
|
72
|
+
end
|
73
|
+
end
|
74
|
+
call(params) do |doc|
|
75
|
+
ret_val = doc.css("BatchPutAttributesResponse").length > 0
|
76
|
+
block.call(ret_val) if block_given?
|
77
|
+
ret_val
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete_attributes(name, id, attributes = {}, expected = {}, &block)
|
82
|
+
params = define_attributes(attributes, expected)
|
83
|
+
params.merge!({
|
84
|
+
'DomainName' => name,
|
85
|
+
'ItemName' => id,
|
86
|
+
'Action' => 'DeleteAttributes'
|
87
|
+
})
|
88
|
+
call(params) do |doc|
|
89
|
+
ret_val = doc.css("DeleteAttributesResponse").length > 0
|
90
|
+
block.call(ret_val) if block_given?
|
91
|
+
ret_val
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_attributes(name, id, wanted_attributes = [], consistent_read = false, &block)
|
96
|
+
params = {
|
97
|
+
'Action' => 'GetAttributes',
|
98
|
+
'DomainName' => name,
|
99
|
+
'ItemName' => id,
|
100
|
+
'ConsistentRead' => consistent_read.to_s,
|
101
|
+
}
|
102
|
+
wanted_attributes.each_with_index {|name, index| params["AttributeName.#{index}"] = name}
|
103
|
+
|
104
|
+
call(params) do |doc|
|
105
|
+
attributes = doc.css("Attribute").inject({}) do |memo, attribute|
|
106
|
+
name = attribute.css("Name").first.text
|
107
|
+
value = attribute.css("Value").first.text
|
108
|
+
if memo.has_key?(name)
|
109
|
+
memo[name] = [memo[name]] unless memo[name].is_a?(Array)
|
110
|
+
memo[name] << value
|
111
|
+
else
|
112
|
+
memo[name] = value
|
113
|
+
end
|
114
|
+
memo
|
115
|
+
end
|
116
|
+
block.call(attributes) if block_given?
|
117
|
+
attributes
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def select(expression, consistent_read = false, next_token = nil, &block)
|
122
|
+
params = {
|
123
|
+
'Action' => 'Select',
|
124
|
+
'SelectExpression' => expression,
|
125
|
+
'ConsistentRead' => consistent_read.to_s,
|
126
|
+
}
|
127
|
+
params['NextToken'] = next_token unless next_token.nil?
|
128
|
+
call(params) do |doc|
|
129
|
+
ret_val = doc.css("SelectResponse SelectResult Item").inject({}) do |items, item|
|
130
|
+
item_name = item.css("Name").first.text
|
131
|
+
items[item_name] = item.css("Attribute").inject({}) do |attributes, attribute|
|
132
|
+
attribute_name = attribute.css("Name").first.text
|
133
|
+
attribute_value = attribute.css("Value").first.text
|
134
|
+
if attributes.has_key?(attribute_name)
|
135
|
+
attributes[attribute_name] = [attributes[attribute_name]] unless attributes[attribute_name].is_a?(Array)
|
136
|
+
attributes[attribute_name] << attribute_value
|
137
|
+
else
|
138
|
+
attributes[attribute_name] = attribute_value
|
139
|
+
end
|
140
|
+
attributes
|
141
|
+
end
|
142
|
+
items
|
143
|
+
end
|
144
|
+
block.call(ret_val) if block_given?
|
145
|
+
ret_val
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def define_attributes(attributes = {}, expected = {}, replace = false)
|
151
|
+
params = {}
|
152
|
+
attributes.sort.inject(0) do |index, (k,v)|
|
153
|
+
v = [v] unless v.is_a?(Array)
|
154
|
+
v.each do |value|
|
155
|
+
params["Attribute.#{index}.Name"] = k
|
156
|
+
params["Attribute.#{index}.Value"] = value
|
157
|
+
params["Attribute.#{index}.Replace"] = "1" if replace
|
158
|
+
index += 1
|
159
|
+
end
|
160
|
+
index
|
161
|
+
end
|
162
|
+
expected.sort.inject(0) {|index, (k,v)|
|
163
|
+
case v
|
164
|
+
when Array
|
165
|
+
v.each do |value|
|
166
|
+
params["Expected.#{index}.Name"] = k
|
167
|
+
params["Expected.#{index}.Value"] = value
|
168
|
+
index += 1
|
169
|
+
end
|
170
|
+
when :exists
|
171
|
+
params["Expected.#{index}.Name"] = k
|
172
|
+
params["Expected.#{index}.Exists"] = v
|
173
|
+
index += 1
|
174
|
+
else
|
175
|
+
params["Expected.#{index}.Name"] = k
|
176
|
+
params["Expected.#{index}.Value"] = v
|
177
|
+
index += 1
|
178
|
+
end
|
179
|
+
index
|
180
|
+
}
|
181
|
+
return params
|
182
|
+
end
|
183
|
+
|
184
|
+
def call(params = {}, attempts = 3, &block)
|
185
|
+
@client.call(:post, params) do |body|
|
186
|
+
begin
|
187
|
+
doc = Nokogiri::XML(body)
|
188
|
+
if error = doc.css("Response Errors Error").first
|
189
|
+
raise SimplyDB::Error.const_get(error.css("Code").first.content), error.css("Message").first.content
|
190
|
+
else
|
191
|
+
#gather some stats from the request
|
192
|
+
@request_id = doc.css("RequestId").first.text
|
193
|
+
@box_usage = doc.css("BoxUsage").first.text.to_f
|
194
|
+
@next_token = doc.css("NextToken").first.text unless doc.css("NextToken").empty?
|
195
|
+
block.call(doc)
|
196
|
+
end
|
197
|
+
rescue SimplyDB::Error::ServiceUnavailable => e
|
198
|
+
if attempts > 0
|
199
|
+
call(params, attempts - 1, &block)
|
200
|
+
else
|
201
|
+
raise
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|