module AwsSdb module Request class Base HOST = 'sdb.amazonaws.com' attr_accessor :account, :secret, :params def initialize(method, params, opts={}) @account = opts[:account] || (ENV['AMAZON_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY_ID']) @secret = opts[:secret] || (ENV['AMAZON_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_ACCESS_KEY']) raise <<-end_msg unless @account && @secret Amazon AWS account or access key not defined Please pass {:account => 'your account', :secret => 'your secret'} as a last argument or define the following environment variables ENV['AMAZON_ACCESS_KEY_ID'] ENV['AMAZON_SECRET_ACCESS_KEY'] end_msg @method = method @params = params add_req_data_to_params end #Hostname for the request def host HOST end #Uri path def path '/' end #The full uri for the request, it takes the protocol as argument def uri(protocol = 'http') "#{protocol}://" + host + path + '?' + uri_query end #Only the query part of the uri def uri_query params_query + '&Signature=' + signature end alias :to_s :uri private def add_req_data_to_params @params.update({'Version' => '2007-11-07', 'SignatureVersion' => '2', 'SignatureMethod' => 'HmacSHA256', 'AWSAccessKeyId' => @account, 'Timestamp' => Time.now.gmtime.iso8601}) end def params_query @params_query ||= @params.keys.sort.map do |key| key + '=' + escape(@params[key].to_s) end.join('&') end def data_to_sign [@method, host, path, params_query].join("\n") end def signature @signature ||= begin digest = OpenSSL::Digest::Digest.new('sha256') hmac = OpenSSL::HMAC.digest(digest, @secret, data_to_sign) escape(Base64.encode64(hmac).strip) end end def escape(string) URI.escape(string, /[^-_.~a-zA-Z\d]/) end end #Is the parent of all the request templates, defines some methods class Template class << self #Generates method to allow shorter expressions in parameter #definition takes an hash # # shortcuts({'LongUrlQueryParams' => :abbr}) # #will cause :abbr params to be expanded in LongUrlQueryParams def shortcuts(shortcuts_hash) shortcuts_hash.each do |full_key, shortcut| define_method("#{shortcut}=") do |value| @params[full_key] = value end end end end #takes the params for the request and an optional options hash #params for the request are request specific, options accepted are only #AWS credentials in form of # # {:account => 'your account', :secret => 'your secret'} def initialize(params={}, opts={}) @params, @opts = params, opts end def token=(value) @params['NextToken'] = value end def attributes=(attributes) attributes.each_with_index do |attribute, index| @params["AttributeName.#{index}"] = attribute.to_s end end def request expand! @request ||= begin Base.new('GET', @params.merge('Action' => self.class.to_s.split(':')[-1]), @opts) end end [:to_s, :uri, :uri_query].each do |method| define_method(method) do request.send(method) end end private def expand! @params.keys.each do |key| send("#{key}=", @params.delete(key)) if respond_to?("#{key}=") end @params end end #List the domains # # ListDomains.new({:max => 4}, {:account => 'account', :secret => 'secret'}) # #Shortcut in params: # # :max - the maximum number of domain that can be returned # :token - the token for follow up requests class ListDomains < Template shortcuts({'MaxNumberOfDomains' => :max}) end #Creates a new domain # # CreateDomain.new({:name => 'my_domain'}) # #Shortcut in params: # # :name - the name for the new domain class CreateDomain < Template shortcuts({'DomainName' => :name}) end #Deletes a domain # # DeleteDomain.new({:name => 'my_domain'}) # #Shortcut in params: # # :name - the name for domain class DeleteDomain < Template shortcuts({'DomainName' => :name}) end #Provides informations for a domain # # DomainMetadata.new({:name => 'my_domain'}) # #Shortcut in params: # # :name - the name for domain class DomainMetadata < Template shortcuts({'DomainName' => :name}) end #Get the attributes for an item # # GetAttributes.new({:name => 'my_item', :attributes => [:first, :second]}) # #Shortcut in params: # # :name - the item name # :attributes - the list of the attributes to fetch, if not specified # fetches all the attributes class GetAttributes < Template shortcuts({'ItemName' => :name, 'DomainName' => :domain}) end #Add attributes to an item or create a new item whith the given attributes # # req = PutAttributes.new({:name => 'my_item'}) # req.attributes = {:color => :black, :shape => {:value => :square, :replace => true}} # #Note: in this request the value 'black' will be added to other color #values if they already exist, while the shape value 'square' will replace #pre existing values of shape #the request, adding the :attributes to the params hash in the new method #would have the same effect # #Note: in the sample above attributes are defined after initializing #the request, adding the :attributes to the params hash in the new method #would have the same effect class PutAttributes < Template attr_accessor :attributes class << self #multiple PutAttributes requests can be batched in one BatchPutAttributes #just define the single PutAttributes and then pass them as args to #the PutAttributes::batch method and you'll be returned the batched #request. #At the moment of writing the maximum number of requests you can batch #are 25 def batch(*requests) items_params = requests.map { |r| r.send(:expand!) } first_request = requests.first.request account = first_request.account secret = first_request.secret batch_params = {} batch_params['DomainName'] ||= first_request.params['DomainName'] items_params.each_with_index do |params, index| params.keys.each do |key| batch_params["Item.#{index.to_s + '.' + key}"] = params[key] if key =~ /^ItemName|Attribute/ end end BatchPutAttributes.new(batch_params, {:account => account, :secret => secret}) end end shortcuts({'ItemName' => :name, 'DomainName' => :domain}) def expand_attributes! index = 0 @attributes.keys.each do |attr_name| replace = nil values = case @attributes[attr_name] when Hash replace = @attributes[attr_name][:replace].to_s @attributes[attr_name][:value].to_s else @attributes[attr_name].to_s end values = [values] unless values.is_a?(Array) values.each do |value| @params["Attribute.#{index}.Replace"] = replace if replace @params["Attribute.#{index}.Name"] = attr_name.to_s @params["Attribute.#{index}.Value"] = value index += 1 end end end private def expand! super expand_attributes! end end class BatchPutAttributes < Template shortcuts({'DomainName' => :domain}) end #delete some attributes from the given item, if no attributes #are provided the whole item is deleted # # DeleteAttributes.new(:domain => 'test_domain', :name => 'item_1' # :attributes => [:color, :shape]) class DeleteAttributes < Template shortcuts({'ItemName' => :name, 'DomainName' => :domain}) end #execute a select statement # Select.new(:query => "SELECT * FROM test_domain") class Select < Template shortcuts({'SelectExpression' => :query}) end #execute a query statement returns the array of the item names #satisfying the statment # # Query.new(:domain => 'test_domain', :query => "['shape' = 'square']") class Query < Template shortcuts({'DomainName' => :domain, 'QueryExpression' => :query, 'MaxNumberOfItems' => :max}) end #execute a query with attributes statement returns the items #satisfying statement # # QueryWithAttributes.new(:domain => 'test_domain', :query => "['shape' = 'square']") class QueryWithAttributes < Template shortcuts({'DomainName' => :domain, 'QueryExpression' => :query, 'MaxNumberOfItems' => :max}) end end end