lib/sdb/active_sdb.rb in icehouse-right_aws-1.11.0 vs lib/sdb/active_sdb.rb in icehouse-right_aws-2.2.0
- old
+ new
@@ -19,17 +19,10 @@
# 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.
#
-begin
- require 'uuidtools'
-rescue LoadError => e
- STDERR.puts("RightSDB requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
- exit
-end
-
module RightAws
# = RightAws::ActiveSdb -- RightScale SDB interface (alpha release)
# The RightAws::ActiveSdb class provides a complete interface to Amazon's Simple
# Database Service.
@@ -37,13 +30,10 @@
# ActiveSdb is in alpha and does not load by default with the rest of RightAws. You must use an additional require statement to load the ActiveSdb class. For example:
#
# require 'right_aws'
# require 'sdb/active_sdb'
#
- # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by RightAws and is not installed as a
- # dependency of RightAws.
- #
# Simple ActiveSdb usage example:
#
# class Client < RightAws::ActiveSdb::Base
# end
#
@@ -90,10 +80,63 @@
# sandy.delete_attributes('country', 'gender')
#
# # remove domain
# Client.delete_domain
#
+ # # Dynamic attribute accessors
+ #
+ # class KdClient < RightAws::ActiveSdb::Base
+ # end
+ #
+ # client = KdClient.select(:all, :order => 'expiration').first
+ # pp client.attributes #=>
+ # {"name"=>["Putin"],
+ # "post"=>["president"],
+ # "country"=>["Russia"],
+ # "expiration"=>["2008"],
+ # "id"=>"376d2e00-75b0-11dd-9557-001bfc466dd7",
+ # "gender"=>["male"]}
+ #
+ # pp client.name #=> ["Putin"]
+ # pp client.country #=> ["Russia"]
+ # pp client.post #=> ["president"]
+ #
+ # # Columns and simple typecasting
+ #
+ # class Person < RightAws::ActiveSdb::Base
+ # columns do
+ # name
+ # email
+ # score :Integer
+ # is_active :Boolean
+ # registered_at :DateTime
+ # created_at :DateTime, :default => lambda{ Time.now }
+ # end
+ # end
+ # Person::create( :name => 'Yetta E. Andrews', :email => 'nulla.facilisis@metus.com', :score => 100, :is_active => true, :registered_at => Time.local(2000, 1, 1) )
+ #
+ # person = Person.find_by_email 'nulla.facilisis@metus.com'
+ # person.reload
+ #
+ # pp person.attributes #=>
+ # {"name"=>["Yetta E. Andrews"],
+ # "created_at"=>["2010-04-02T20:51:58+0400"],
+ # "id"=>"0ee24946-3e60-11df-9d4c-0025b37efad0",
+ # "registered_at"=>["2000-01-01T00:00:00+0300"],
+ # "is_active"=>["T"],
+ # "score"=>["100"],
+ # "email"=>["nulla.facilisis@metus.com"]}
+ # pp person.name #=> "Yetta E. Andrews"
+ # pp person.name.class #=> String
+ # pp person.registered_at.to_s #=> "2000-01-01T00:00:00+03:00"
+ # pp person.registered_at.class #=> DateTime
+ # pp person.is_active #=> true
+ # pp person.is_active.class #=> TrueClass
+ # pp person.score #=> 100
+ # pp person.score.class #=> Fixnum
+ # pp person.created_at.to_s #=> "2010-04-02T20:51:58+04:00"
+ #
class ActiveSdb
module ActiveSdbConnect
def connection
@connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
@@ -104,11 +147,10 @@
# Params:
# { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
# :port => 443 # Amazon service port: 80 or 443(default)
# :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
# :signature_version => '0' # The signature version : '0' or '1'(default)
- # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
# :logger => Logger Object # Logger instance: logs to STDOUT if omitted
# :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
@connection = RightAws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
@@ -240,11 +282,35 @@
# Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
#
def delete_domain
connection.delete_domain(domain)
end
-
+
+ def columns(&block)
+ @columns ||= ColumnSet.new
+ @columns.instance_eval(&block) if block
+ @columns
+ end
+
+ def column?(col_name)
+ columns.include?(col_name)
+ end
+
+ def type_of(col_name)
+ columns.type_of(col_name)
+ end
+
+ def serialize(attribute, value)
+ s = serialization_for_type(type_of(attribute))
+ s ? s.serialize(value) : value.to_s
+ end
+
+ def deserialize(attribute, value)
+ s = serialization_for_type(type_of(attribute))
+ s ? s.deserialize(value) : value
+ end
+
# Perform a find request.
#
# Single record:
#
# Client.find(:first)
@@ -362,11 +428,11 @@
else select_from_ids args, options
end
end
def generate_id # :nodoc:
- UUIDTools::UUID.timestamp_create().to_s
+ AwsUtils::generate_unique_token
end
protected
# Select
@@ -374,17 +440,17 @@
def select_from_ids(args, options) # :nodoc:
cond = []
# detect amount of records requested
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
# flatten ids
- args = args.to_a.flatten
+ args = Array(args).flatten
args.each { |id| cond << "id=#{self.connection.escape(id)}" }
ids_cond = "(#{cond.join(' OR ')})"
# user defined :conditions to string (if it was defined)
options[:conditions] = build_conditions(options[:conditions])
# join ids condition and user defined conditions
- options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
+ options[:conditions] = options[:conditions].right_blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
result = sql_select(options)
# if one record was requested then return it
unless bunch_of_records_requested
record = result.first
# railse if nothing was found
@@ -469,11 +535,11 @@
sort_by_expression = " sort '#{sort_by}' #{sort_order}"
# make query_expression to be a string (it may be null)
query_expression = query_expression.to_s
# quote from Amazon:
# The sort attribute must be present in at least one of the predicates of the query expression.
- if query_expression.blank?
+ if query_expression.right_blank?
query_expression = sort_query_expression
elsif !query_attributes(query_expression).include?(sort_by)
query_expression += " intersection #{sort_query_expression}"
end
query_expression += sort_by_expression
@@ -516,17 +582,17 @@
def find_from_ids(args, options) # :nodoc:
cond = []
# detect amount of records requested
bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
# flatten ids
- args = args.to_a.flatten
+ args = Array(args).flatten
args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
ids_cond = "[#{cond.join(' OR ')}]"
# user defined :conditions to string (if it was defined)
options[:conditions] = build_conditions(options[:conditions])
# join ids condition and user defined conditions
- options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
+ options[:conditions] = options[:conditions].right_blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
result = find_every(options)
# if one record was requested then return it
unless bunch_of_records_requested
record = result.first
# railse if nothing was found
@@ -573,13 +639,13 @@
from = options[:from] || domain
conditions = options[:conditions] ? " WHERE #{build_conditions(options[:conditions])}" : ''
order = options[:order] ? " ORDER BY #{options[:order]}" : ''
limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
# mix sort by argument (it must present in response)
- unless order.blank?
+ unless order.right_blank?
sort_by, sort_order = sort_options(options[:order])
- conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
+ conditions << (conditions.right_blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
end
"SELECT #{select} FROM #{from}#{conditions}#{order}#{limit}"
end
def build_conditions(conditions) # :nodoc:
@@ -588,10 +654,17 @@
when conditions.is_a?(Hash) then connection.query_expression_from_hash(conditions)
else conditions
end
end
+ def serialization_for_type(type)
+ @serializations ||= {}
+ unless @serializations.has_key? type
+ @serializations[type] = ::RightAws::ActiveSdb.const_get("#{type}Serialization") rescue false
+ end
+ @serializations[type]
+ end
end
public
# instance attributes
@@ -655,15 +728,19 @@
# puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
#
def attributes=(attrs)
old_id = @attributes['id']
@attributes = uniq_values(attrs)
- @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
+ @attributes['id'] = old_id if @attributes['id'].right_blank? && !old_id.right_blank?
self.attributes
end
- def connection
+ def columns
+ self.class.columns
+ end
+
+ def connection
self.class.connection
end
# Item domain name.
def domain
@@ -673,22 +750,30 @@
# Returns the values of the attribute identified by +attribute+.
#
# puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
#
def [](attribute)
- @attributes[attribute.to_s]
+ raw = @attributes[attribute.to_s]
+ self.class.column?(attribute) && raw ? self.class.deserialize(attribute, raw.first) : raw
end
# Updates the attribute identified by +attribute+ with the specified +values+.
#
# puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
# item['Cat'] = ["Whiskas", "chicken"]
# puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
#
def []=(attribute, values)
attribute = attribute.to_s
- @attributes[attribute] = attribute == 'id' ? values.to_s : values.to_a.uniq
+ @attributes[attribute] = case
+ when attribute == 'id'
+ values.to_s
+ when self.class.column?(attribute)
+ self.class.serialize(attribute, values)
+ else
+ Array(values).uniq
+ end
end
# Reload attributes from SDB. Replaces in-memory attributes.
#
# item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
@@ -697,11 +782,11 @@
def reload
raise_on_id_absence
old_id = id
attrs = connection.get_attributes(domain, id)[:attributes]
@attributes = {}
- unless attrs.blank?
+ unless attrs.right_blank?
attrs.each { |attribute, values| @attributes[attribute] = values }
@attributes['id'] = old_id
end
mark_as_old
@attributes
@@ -723,11 +808,11 @@
attrs_list.delete('id')
result = {}
attrs_list.flatten.uniq.each do |attribute|
attribute = attribute.to_s
values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
- unless values.blank?
+ unless values.right_blank?
@attributes[attribute] = result[attribute] = values
else
@attributes.delete(attribute)
end
end
@@ -753,11 +838,11 @@
def put
@attributes = uniq_values(@attributes)
prepare_for_update
attrs = @attributes.dup
attrs.delete('id')
- connection.put_attributes(domain, id, attrs) unless attrs.blank?
+ connection.put_attributes(domain, id, attrs) unless attrs.right_blank?
connection.put_attributes(domain, id, { 'id' => id }, :replace)
mark_as_old
@attributes
end
@@ -769,14 +854,14 @@
def put_attributes(attrs)
attrs = uniq_values(attrs)
prepare_for_update
# if 'id' is present in attrs hash:
# replace internal 'id' attribute and remove it from the attributes to be sent
- @attributes['id'] = attrs['id'] unless attrs['id'].blank?
+ @attributes['id'] = attrs['id'] unless attrs['id'].right_blank?
attrs.delete('id')
# add new values to all attributes from list
- connection.put_attributes(domain, id, attrs) unless attrs.blank?
+ connection.put_attributes(domain, id, attrs) unless attrs.right_blank?
connection.put_attributes(domain, id, { 'id' => id }, :replace)
attrs.each do |attribute, values|
@attributes[attribute] ||= []
@attributes[attribute] += values
@attributes[attribute].uniq!
@@ -816,16 +901,16 @@
# see +save+ method
def save_attributes(attrs)
prepare_for_update
attrs = uniq_values(attrs)
# if 'id' is present in attrs hash then replace internal 'id' attribute
- unless attrs['id'].blank?
+ unless attrs['id'].right_blank?
@attributes['id'] = attrs['id']
else
attrs['id'] = id
end
- connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.right_blank?
attrs.each { |attribute, values| attrs[attribute] = values }
mark_as_old
attrs
end
@@ -840,11 +925,11 @@
#
def delete_values(attrs)
raise_on_id_absence
attrs = uniq_values(attrs)
attrs.delete('id')
- unless attrs.blank?
+ unless attrs.right_blank?
connection.delete_attributes(domain, id, attrs)
attrs.each do |attribute, values|
# remove the values from the attribute
if @attributes[attribute]
@attributes[attribute] -= values
@@ -869,11 +954,11 @@
#
def delete_attributes(*attrs_list)
raise_on_id_absence
attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
attrs_list.delete('id')
- unless attrs_list.blank?
+ unless attrs_list.right_blank?
connection.delete_attributes(domain, id, attrs_list)
attrs_list.each { |attribute| @attributes.delete(attribute) }
end
attrs_list
end
@@ -904,27 +989,119 @@
def mark_as_old # :nodoc:
@new_record = false
end
- private
-
+ # support accessing attribute values via method call
+ def method_missing(method_sym, *args)
+ method_name = method_sym.to_s
+ setter = method_name[-1,1] == '='
+ method_name.chop! if setter
+
+ if @attributes.has_key?(method_name) || self.class.column?(method_name)
+ setter ? self[method_name] = args.first : self[method_name]
+ else
+ super
+ end
+ end
+
+ private
+
def raise_on_id_absence
raise ActiveSdbError.new('Unknown record id') unless id
end
def prepare_for_update
- @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
+ @attributes['id'] = self.class.generate_id if @attributes['id'].right_blank?
+ columns.all.each do |col_name|
+ self[col_name] ||= columns.default(col_name)
+ end
end
def uniq_values(attributes=nil) # :nodoc:
attrs = {}
attributes.each do |attribute, values|
attribute = attribute.to_s
- attrs[attribute] = attribute == 'id' ? values.to_s : values.to_a.uniq
- attrs.delete(attribute) if values.blank?
+ attrs[attribute] = case
+ when attribute == 'id'
+ values.to_s
+ when self.class.column?(attribute)
+ values.is_a?(String) ? values : self.class.serialize(attribute, values)
+ else
+ Array(values).uniq
+ end
+ attrs.delete(attribute) if values.right_blank?
end
attrs
end
end
+
+ class ColumnSet
+ attr_accessor :columns
+ def initialize
+ @columns = {}
+ end
+
+ def all
+ @columns.keys
+ end
+
+ def column(col_name)
+ @columns[col_name.to_s]
+ end
+ alias_method :include?, :column
+
+ def type_of(col_name)
+ column(col_name) && column(col_name)[:type]
+ end
+
+ def default(col_name)
+ return nil unless include?(col_name)
+ default = column(col_name)[:default]
+ default.respond_to?(:call) ? default.call : default
+ end
+
+ def method_missing(method_sym, *args)
+ data_type = args.shift || :String
+ options = args.shift || {}
+ @columns[method_sym.to_s] = options.merge( :type => data_type )
+ end
+ end
+
+ class DateTimeSerialization
+ class << self
+ def serialize(date)
+ date.strftime('%Y-%m-%dT%H:%M:%S%z')
+ end
+
+ def deserialize(string)
+ r = DateTime.parse(string) rescue nil
+ end
+ end
+ end
+
+ class BooleanSerialization
+ class << self
+ def serialize(boolean)
+ boolean ? 'T' : 'F'
+ end
+
+ def deserialize(string)
+ string == 'T'
+ end
+ end
+ end
+
+ class IntegerSerialization
+ class << self
+ def serialize(int)
+ int.to_s
+ end
+
+ def deserialize(string)
+ string.to_i
+ end
+ end
+ end
+
end
end