appoxy-aws 1.11.14 → 1.11.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,915 @@
1
+ #
2
+ # Copyright (c) 2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ begin
25
+ require 'uuidtools'
26
+ rescue LoadError => e
27
+ STDERR.puts("RightSDB requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
28
+ exit
29
+ end
30
+
31
+ module RightAws
32
+
33
+ # = RightAws::ActiveSdb -- RightScale SDB interface (alpha release)
34
+ # The RightAws::ActiveSdb class provides a complete interface to Amazon's Simple
35
+ # Database Service.
36
+ #
37
+ # 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:
38
+ #
39
+ # require 'right_aws'
40
+ # require 'sdb/active_sdb'
41
+ #
42
+ # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by RightAws and is not installed as a
43
+ # dependency of RightAws.
44
+ #
45
+ # Simple ActiveSdb usage example:
46
+ #
47
+ # class Client < RightAws::ActiveSdb::Base
48
+ # end
49
+ #
50
+ # # connect to SDB
51
+ # RightAws::ActiveSdb.establish_connection
52
+ #
53
+ # # create domain
54
+ # Client.create_domain
55
+ #
56
+ # # create initial DB
57
+ # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
58
+ # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
59
+ # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
60
+ # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
61
+ # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
62
+ # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
63
+ #
64
+ # # find all Bushes in USA
65
+ # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
66
+ # client.reload
67
+ # puts client.attributes.inspect
68
+ # end
69
+ #
70
+ # # find all Maries through the world
71
+ # Client.find_all_by_name_and_gender('Mary','female').each do |mary|
72
+ # mary.reload
73
+ # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
74
+ # end
75
+ #
76
+ # # find new russian president
77
+ # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
78
+ # if medvedev
79
+ # medvedev.reload
80
+ # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
81
+ # end
82
+ #
83
+ # # retire old president
84
+ # Client.find_by_name('Putin').delete
85
+ #
86
+ # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
87
+ # sandy = Client.find(sandy_id)
88
+ # sandy.reload
89
+ # sandy.delete_values('hobby' => ['cooking'] )
90
+ # sandy.delete_attributes('country', 'gender')
91
+ #
92
+ # # remove domain
93
+ # Client.delete_domain
94
+ #
95
+ class ActiveSdb
96
+
97
+ module ActiveSdbConnect
98
+ def connection
99
+ @connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
100
+ end
101
+
102
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
103
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
104
+ # The +params+ are passed through as-is to RightAws::SdbInterface.new
105
+ # Params:
106
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
107
+ # :port => 443 # Amazon service port: 80 or 443(default)
108
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
109
+ # :signature_version => '0' # The signature version : '0' or '1'(default)
110
+ # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
111
+ # :connection_mode => :default # options are :default (will use best known option, may change in the future)
112
+ # :per_request (opens and closes a connection on every request to SDB)
113
+ # :single (same as old multi_thread=>false)
114
+ # :per_thread (same as old multi_thread=>true)
115
+ # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
116
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
117
+ # :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')
118
+
119
+ def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
120
+ @connection = RightAws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
121
+ end
122
+
123
+ def close_connection
124
+ @connection.close_connection unless @connection.nil?
125
+ end
126
+ end
127
+
128
+ class ActiveSdbError < RuntimeError ; end
129
+
130
+ class << self
131
+ include ActiveSdbConnect
132
+
133
+ # Retreive a list of domains.
134
+ #
135
+ # put RightAws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
136
+ #
137
+ def domains
138
+ connection.list_domains[:domains]
139
+ end
140
+
141
+ # Create new domain.
142
+ # Raises no errors if the domain already exists.
143
+ #
144
+ # RightAws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
145
+ #
146
+ def create_domain(domain_name)
147
+ connection.create_domain(domain_name)
148
+ end
149
+
150
+ # Remove domain from SDB.
151
+ # Raises no errors if the domain does not exist.
152
+ #
153
+ # RightAws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
154
+ #
155
+ def delete_domain(domain_name)
156
+ connection.delete_domain(domain_name)
157
+ end
158
+ end
159
+
160
+ class Base
161
+
162
+ class << self
163
+ include ActiveSdbConnect
164
+
165
+ # next_token value returned by last find: is useful to continue finding
166
+ attr_accessor :next_token
167
+
168
+ # Returns a RightAws::SdbInterface object
169
+ #
170
+ # class A < RightAws::ActiveSdb::Base
171
+ # end
172
+ #
173
+ # class B < RightAws::ActiveSdb::Base
174
+ # end
175
+ #
176
+ # class C < RightAws::ActiveSdb::Base
177
+ # end
178
+ #
179
+ # RightAws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
180
+ #
181
+ # C.establish_connection 'key_id_2', 'secret_key_2'
182
+ #
183
+ # # A and B uses the default connection, C - uses its own
184
+ # puts A.connection #=> #<RightAws::SdbInterface:0xb76d6d7c>
185
+ # puts B.connection #=> #<RightAws::SdbInterface:0xb76d6d7c>
186
+ # puts C.connection #=> #<RightAws::SdbInterface:0xb76d6ca0>
187
+ #
188
+ def connection
189
+ @connection || ActiveSdb::connection
190
+ end
191
+
192
+ @domain = nil
193
+
194
+ # Current domain name.
195
+ #
196
+ # # if 'ActiveSupport' is not loaded then class name converted to downcase
197
+ # class Client < RightAws::ActiveSdb::Base
198
+ # end
199
+ # puts Client.domain #=> 'client'
200
+ #
201
+ # # if 'ActiveSupport' is loaded then class name being tableized
202
+ # require 'activesupport'
203
+ # class Client < RightAws::ActiveSdb::Base
204
+ # end
205
+ # puts Client.domain #=> 'clients'
206
+ #
207
+ # # Explicit domain name definition
208
+ # class Client < RightAws::ActiveSdb::Base
209
+ # set_domain_name :foreign_clients
210
+ # end
211
+ # puts Client.domain #=> 'foreign_clients'
212
+ #
213
+ def domain
214
+ unless @domain
215
+ if defined? ActiveSupport::CoreExtensions::String::Inflections
216
+ @domain = name.tableize
217
+ else
218
+ @domain = name.downcase
219
+ end
220
+ end
221
+ @domain
222
+ end
223
+
224
+ # Change the default domain name to user defined.
225
+ #
226
+ # class Client < RightAws::ActiveSdb::Base
227
+ # set_domain_name :foreign_clients
228
+ # end
229
+ #
230
+ def set_domain_name(domain)
231
+ @domain = domain.to_s
232
+ end
233
+
234
+ # Create domain at SDB.
235
+ # Raises no errors if the domain already exists.
236
+ #
237
+ # class Client < RightAws::ActiveSdb::Base
238
+ # end
239
+ # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
240
+ #
241
+ def create_domain
242
+ connection.create_domain(domain)
243
+ end
244
+
245
+ # Remove domain from SDB.
246
+ # Raises no errors if the domain does not exist.
247
+ #
248
+ # class Client < RightAws::ActiveSdb::Base
249
+ # end
250
+ # Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
251
+ #
252
+ def delete_domain
253
+ connection.delete_domain(domain)
254
+ end
255
+
256
+ #
257
+ # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
258
+ #
259
+ def find(*args)
260
+ select(*args)
261
+
262
+ =begin
263
+
264
+ options = args.last.is_a?(Hash) ? args.pop : {}
265
+ case args.first
266
+ when :all then find_every options
267
+ when :first then find_initial options
268
+ else find_from_ids args, options
269
+ end
270
+
271
+ =end
272
+
273
+ end
274
+
275
+ # Perform a SQL-like select request.
276
+ #
277
+ # Single record:
278
+ #
279
+ # Client.select(:first)
280
+ # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
281
+ # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
282
+ #
283
+ # Bunch of records:
284
+ #
285
+ # Client.select(:all)
286
+ # Client.select(:all, :limit => 10)
287
+ # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
288
+ # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
289
+ #
290
+ # Records by ids:
291
+ #
292
+ # Client.select('1')
293
+ # Client.select('1234987b4583475347523948')
294
+ # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
295
+ #
296
+ # Find helpers: RightAws::ActiveSdb::Base.select_by_... and RightAws::ActiveSdb::Base.select_all_by_...
297
+ #
298
+ # Client.select_by_name('Matias Rust')
299
+ # Client.select_by_name_and_city('Putin','Moscow')
300
+ # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
301
+ #
302
+ # Client.select_all_by_author('G.Bush jr')
303
+ # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
304
+ # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
305
+ #
306
+ # Continue listing:
307
+ #
308
+ # # initial listing
309
+ # Client.select(:all, :limit => 10)
310
+ # # continue listing
311
+ # begin
312
+ # Client.select(:all, :limit => 10, :next_token => Client.next_token)
313
+ # end while Client.next_token
314
+ #
315
+ # Sort oder:
316
+ # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
317
+ #
318
+ # Client.select(:all) # returns all records
319
+ # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
320
+ # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
321
+ #
322
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
323
+ #
324
+ def select(*args)
325
+ options = args.last.is_a?(Hash) ? args.pop : {}
326
+ # puts 'first=' + args.first.to_s
327
+ case args.first
328
+ when :all then sql_select(options)
329
+ when :first then sql_select(options.merge(:limit => 1)).first
330
+ else select_from_ids args, options
331
+ end
332
+ end
333
+
334
+ def generate_id # :nodoc:
335
+ UUIDTools::UUID.timestamp_create().to_s
336
+ end
337
+
338
+ protected
339
+
340
+ # Select
341
+
342
+ def select_from_ids(args, options) # :nodoc:
343
+ cond = []
344
+ # detect amount of records requested
345
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
346
+ # flatten ids
347
+ args = args.to_a.flatten
348
+ args.each { |id| cond << "id=#{self.connection.escape(id)}" }
349
+ ids_cond = "(#{cond.join(' OR ')})"
350
+ # user defined :conditions to string (if it was defined)
351
+ options[:conditions] = build_conditions(options[:conditions])
352
+ # join ids condition and user defined conditions
353
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
354
+ result = sql_select(options)
355
+ # if one record was requested then return it
356
+ unless bunch_of_records_requested
357
+ record = result.first
358
+ # railse if nothing was found
359
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
360
+ record
361
+ else
362
+ # if a bunch of records was requested then return check that we found all of them
363
+ # and return as an array
364
+ unless args.size == result.size
365
+ id_list = args.map{|i| "'#{i}'"}.join(',')
366
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
367
+ else
368
+ result
369
+ end
370
+ end
371
+ end
372
+
373
+ def sql_select(options) # :nodoc:
374
+ @next_token = options[:next_token]
375
+ select_expression = build_select(options)
376
+ # request items
377
+ query_result = self.connection.select(select_expression, @next_token)
378
+ @next_token = query_result[:next_token]
379
+ items = query_result[:items].map do |hash|
380
+ id, attributes = hash.shift
381
+ new_item = self.new( attributes.merge({ 'id' => id }))
382
+ new_item.mark_as_old
383
+ new_item
384
+ end
385
+ items
386
+ end
387
+
388
+ # select_by helpers
389
+ def select_all_by_(format_str, args, options) # :nodoc:
390
+ fields = format_str.to_s.sub(/^select_(all_)?by_/,'').split('_and_')
391
+ conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
392
+ options[:conditions] = [conditions, *args]
393
+ select(:all, options)
394
+ end
395
+
396
+ def select_by_(format_str, args, options) # :nodoc:
397
+ options[:limit] = 1
398
+ select_all_by_(format_str, args, options).first
399
+ end
400
+
401
+ # Query
402
+
403
+ # Returns an array of query attributes.
404
+ # Query_expression must be a well formated SDB query string:
405
+ # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
406
+ def query_attributes(query_expression) # :nodoc:
407
+ attrs = []
408
+ array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
409
+ until array.empty? do
410
+ attrs << array.shift # skip it's value
411
+ array.shift #
412
+ end
413
+ attrs
414
+ end
415
+
416
+ # Returns an array of [attribute_name, 'asc'|'desc']
417
+ def sort_options(sort_string)
418
+ sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
419
+ [$1, ($2 || 'asc')]
420
+ end
421
+
422
+ # Perform a query request.
423
+ #
424
+ # Options
425
+ # :query_expression nil | string | array
426
+ # :max_number_of_items nil | integer
427
+ # :next_token nil | string
428
+ # :sort_option nil | string "name desc|asc"
429
+ #
430
+ def query(options) # :nodoc:
431
+ @next_token = options[:next_token]
432
+ query_expression = build_conditions(options[:query_expression])
433
+ # add sort_options to the query_expression
434
+ if options[:sort_option]
435
+ sort_by, sort_order = sort_options(options[:sort_option])
436
+ sort_query_expression = "['#{sort_by}' starts-with '']"
437
+ sort_by_expression = " sort '#{sort_by}' #{sort_order}"
438
+ # make query_expression to be a string (it may be null)
439
+ query_expression = query_expression.to_s
440
+ # quote from Amazon:
441
+ # The sort attribute must be present in at least one of the predicates of the query expression.
442
+ if query_expression.blank?
443
+ query_expression = sort_query_expression
444
+ elsif !query_attributes(query_expression).include?(sort_by)
445
+ query_expression += " intersection #{sort_query_expression}"
446
+ end
447
+ query_expression += sort_by_expression
448
+ end
449
+ # request items
450
+ query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token)
451
+ @next_token = query_result[:next_token]
452
+ items = query_result[:items].map do |name|
453
+ new_item = self.new('id' => name)
454
+ new_item.mark_as_old
455
+ reload_if_exists(record) if options[:auto_load]
456
+ new_item
457
+ end
458
+ items
459
+ end
460
+
461
+ # reload a record unless it is nil
462
+ def reload_if_exists(record) # :nodoc:
463
+ record && record.reload
464
+ end
465
+
466
+ def reload_all_records(*list) # :nodoc:
467
+ list.flatten.each { |record| reload_if_exists(record) }
468
+ end
469
+
470
+ def find_every(options) # :nodoc:
471
+ records = query( :query_expression => options[:conditions],
472
+ :max_number_of_items => options[:limit],
473
+ :next_token => options[:next_token],
474
+ :sort_option => options[:sort] || options[:order] )
475
+ options[:auto_load] ? reload_all_records(records) : records
476
+ end
477
+
478
+ def find_initial(options) # :nodoc:
479
+ options[:limit] = 1
480
+ record = find_every(options).first
481
+ options[:auto_load] ? reload_all_records(record).first : record
482
+ end
483
+
484
+ def find_from_ids(args, options) # :nodoc:
485
+ cond = []
486
+ # detect amount of records requested
487
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
488
+ # flatten ids
489
+ args = args.to_a.flatten
490
+ args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
491
+ ids_cond = "[#{cond.join(' OR ')}]"
492
+ # user defined :conditions to string (if it was defined)
493
+ options[:conditions] = build_conditions(options[:conditions])
494
+ # join ids condition and user defined conditions
495
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
496
+ result = find_every(options)
497
+ # if one record was requested then return it
498
+ unless bunch_of_records_requested
499
+ record = result.first
500
+ # railse if nothing was found
501
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
502
+ options[:auto_load] ? reload_all_records(record).first : record
503
+ else
504
+ # if a bunch of records was requested then return check that we found all of them
505
+ # and return as an array
506
+ unless args.size == result.size
507
+ id_list = args.map{|i| "'#{i}'"}.join(',')
508
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
509
+ else
510
+ options[:auto_load] ? reload_all_records(result) : result
511
+ end
512
+ end
513
+ end
514
+
515
+ # find_by helpers
516
+ def find_all_by_(format_str, args, options) # :nodoc:
517
+ fields = format_str.to_s.sub(/^find_(all_)?by_/,'').split('_and_')
518
+ conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
519
+ options[:conditions] = [conditions, *args]
520
+ find(:all, options)
521
+ end
522
+
523
+ def find_by_(format_str, args, options) # :nodoc:
524
+ options[:limit] = 1
525
+ find_all_by_(format_str, args, options).first
526
+ end
527
+
528
+ # Misc
529
+
530
+ def method_missing(method, *args) # :nodoc:
531
+ if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
532
+ # get rid of the find ones, only select now
533
+ to_send_to = $1
534
+ attributes = method.to_s[$1.length..method.to_s.length]
535
+ # puts 'attributes=' + attributes
536
+ if to_send_to[0...4] == "find"
537
+ to_send_to = "select" + to_send_to[4..to_send_to.length]
538
+ # puts 'CONVERTED ' + $1 + " to " + to_send_to
539
+ end
540
+
541
+ options = args.last.is_a?(Hash) ? args.pop : {}
542
+ __send__(to_send_to, attributes, args, options)
543
+ else
544
+ super(method, *args)
545
+ end
546
+ end
547
+
548
+ def build_select(options) # :nodoc:
549
+ select = options[:select] || '*'
550
+ from = options[:from] || domain
551
+ conditions = options[:conditions] ? " WHERE #{build_conditions(options[:conditions])}" : ''
552
+ order = options[:order] ? " ORDER BY #{options[:order]}" : ''
553
+ limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
554
+ # mix sort by argument (it must present in response)
555
+ unless order.blank?
556
+ sort_by, sort_order = sort_options(options[:order])
557
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
558
+ end
559
+ "SELECT #{select} FROM #{from}#{conditions}#{order}#{limit}"
560
+ end
561
+
562
+ def build_conditions(conditions) # :nodoc:
563
+ case
564
+ when conditions.is_a?(Array) then connection.query_expression_from_array(conditions)
565
+ when conditions.is_a?(Hash) then connection.query_expression_from_hash(conditions)
566
+ else conditions
567
+ end
568
+ end
569
+
570
+ end
571
+
572
+ public
573
+
574
+ # instance attributes
575
+ attr_accessor :attributes
576
+
577
+ # item name
578
+ attr_accessor :id
579
+
580
+ # Create new Item instance.
581
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
582
+ #
583
+ # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
584
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
585
+ # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
586
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
587
+ #
588
+ def initialize(attrs={})
589
+ @attributes = uniq_values(attrs)
590
+ @new_record = true
591
+ end
592
+
593
+ # Create and save new Item instance.
594
+ # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
595
+ #
596
+ # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
597
+ # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
598
+ #
599
+ def self.create(attributes={})
600
+ item = self.new(attributes)
601
+ item.save
602
+ item
603
+ end
604
+
605
+ # Returns an item id. Same as: item['id'] or item.attributes['id']
606
+ def id
607
+ @attributes['id']
608
+ end
609
+
610
+ # Sets an item id.
611
+ def id=(id)
612
+ @attributes['id'] = id.to_s
613
+ end
614
+
615
+ # Returns a hash of all the attributes.
616
+ #
617
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
618
+ #
619
+ def attributes
620
+ @attributes.dup
621
+ end
622
+
623
+ # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
624
+ # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
625
+ #
626
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
627
+ # # set new attributes ('id' is missed)
628
+ # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
629
+ # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
630
+ # # set new attributes ('id' is set)
631
+ # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
632
+ # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
633
+ #
634
+ def attributes=(attrs)
635
+ old_id = @attributes['id']
636
+ @attributes = uniq_values(attrs)
637
+ @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
638
+ self.attributes
639
+ end
640
+
641
+ def connection
642
+ self.class.connection
643
+ end
644
+
645
+ # Item domain name.
646
+ def domain
647
+ self.class.domain
648
+ end
649
+
650
+ # Returns the values of the attribute identified by +attribute+.
651
+ #
652
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
653
+ #
654
+ def [](attribute)
655
+ @attributes[attribute.to_s]
656
+ end
657
+
658
+ # Updates the attribute identified by +attribute+ with the specified +values+.
659
+ #
660
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
661
+ # item['Cat'] = ["Whiskas", "chicken"]
662
+ # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
663
+ #
664
+ def []=(attribute, values)
665
+ attribute = attribute.to_s
666
+ @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
667
+ end
668
+
669
+ # Reload attributes from SDB. Replaces in-memory attributes.
670
+ #
671
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
672
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
673
+ #
674
+ def reload
675
+ raise_on_id_absence
676
+ old_id = id
677
+ attrs = connection.get_attributes(domain, id)[:attributes]
678
+ @attributes = {}
679
+ unless attrs.blank?
680
+ attrs.each { |attribute, values| @attributes[attribute] = values }
681
+ @attributes['id'] = old_id
682
+ end
683
+ mark_as_old
684
+ @attributes
685
+ end
686
+
687
+ # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
688
+ # +attrs_list+ is an array or comma separated list of attributes names.
689
+ # Returns a hash of loaded attributes.
690
+ #
691
+ # This is not the best method to get a bunch of attributes because
692
+ # a web service call is being performed for every attribute.
693
+ #
694
+ # item = Client.find_by_name('Cat')
695
+ # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
696
+ #
697
+ def reload_attributes(*attrs_list)
698
+ raise_on_id_absence
699
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
700
+ attrs_list.delete('id')
701
+ result = {}
702
+ attrs_list.flatten.uniq.each do |attribute|
703
+ attribute = attribute.to_s
704
+ values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
705
+ unless values.blank?
706
+ @attributes[attribute] = result[attribute] = values
707
+ else
708
+ @attributes.delete(attribute)
709
+ end
710
+ end
711
+ mark_as_old
712
+ result
713
+ end
714
+
715
+ # Stores in-memory attributes to SDB.
716
+ # Adds the attributes values to already stored at SDB.
717
+ # Returns a hash of stored attributes.
718
+ #
719
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
720
+ # sandy['toys'] = 'boys'
721
+ # sandy.put
722
+ # sandy['toys'] = 'patchwork'
723
+ # sandy.put
724
+ # sandy['toys'] = 'kids'
725
+ # sandy.put
726
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
727
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
728
+ #
729
+ # compare to +save+ method
730
+ def put
731
+ @attributes = uniq_values(@attributes)
732
+ prepare_for_update
733
+ attrs = @attributes.dup
734
+ attrs.delete('id')
735
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
736
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
737
+ mark_as_old
738
+ @attributes
739
+ end
740
+
741
+ # Stores specified attributes.
742
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
743
+ # Returns a hash of saved attributes.
744
+ #
745
+ # see to +put+ method
746
+ def put_attributes(attrs)
747
+ attrs = uniq_values(attrs)
748
+ prepare_for_update
749
+ # if 'id' is present in attrs hash:
750
+ # replace internal 'id' attribute and remove it from the attributes to be sent
751
+ @attributes['id'] = attrs['id'] unless attrs['id'].blank?
752
+ attrs.delete('id')
753
+ # add new values to all attributes from list
754
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
755
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
756
+ attrs.each do |attribute, values|
757
+ @attributes[attribute] ||= []
758
+ @attributes[attribute] += values
759
+ @attributes[attribute].uniq!
760
+ end
761
+ mark_as_old
762
+ attributes
763
+ end
764
+
765
+ # Store in-memory attributes to SDB.
766
+ # Replaces the attributes values already stored at SDB by in-memory data.
767
+ # Returns a hash of stored attributes.
768
+ #
769
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
770
+ # sandy['toys'] = 'boys'
771
+ # sandy.put
772
+ # sandy['toys'] = 'patchwork'
773
+ # sandy.put
774
+ # sandy['toys'] = 'kids'
775
+ # sandy.put
776
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
777
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
778
+ #
779
+ # compare to +put+ method
780
+ def save
781
+ pre_save2
782
+ connection.put_attributes(domain, id, @attributes, :replace)
783
+ apres_save2
784
+ @attributes
785
+ end
786
+
787
+ def pre_save2
788
+ @attributes = uniq_values(@attributes)
789
+ prepare_for_update
790
+ end
791
+
792
+ def apres_save2
793
+ mark_as_old
794
+ end
795
+
796
+ # Replaces the attributes at SDB by the given values.
797
+ # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
798
+ # The other in-memory attributes are not being saved.
799
+ # Returns a hash of stored attributes.
800
+ #
801
+ # see +save+ method
802
+ def save_attributes(attrs)
803
+ prepare_for_update
804
+ attrs = uniq_values(attrs)
805
+ # if 'id' is present in attrs hash then replace internal 'id' attribute
806
+ unless attrs['id'].blank?
807
+ @attributes['id'] = attrs['id']
808
+ else
809
+ attrs['id'] = id
810
+ end
811
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
812
+ attrs.each { |attribute, values| attrs[attribute] = values }
813
+ mark_as_old
814
+ attrs
815
+ end
816
+
817
+ # Remove specified values from corresponding attributes.
818
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
819
+ #
820
+ # sandy = Client.find_by_name 'Sandy'
821
+ # sandy.reload
822
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
823
+ # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
824
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
825
+ #
826
+ def delete_values(attrs)
827
+ raise_on_id_absence
828
+ attrs = uniq_values(attrs)
829
+ attrs.delete('id')
830
+ unless attrs.blank?
831
+ connection.delete_attributes(domain, id, attrs)
832
+ attrs.each do |attribute, values|
833
+ # remove the values from the attribute
834
+ if @attributes[attribute]
835
+ @attributes[attribute] -= values
836
+ else
837
+ # if the attribute is unknown remove it from a resulting list of fixed attributes
838
+ attrs.delete(attribute)
839
+ end
840
+ end
841
+ end
842
+ attrs
843
+ end
844
+
845
+ # Removes specified attributes from the item.
846
+ # +attrs_list+ is an array or comma separated list of attributes names.
847
+ # Returns the list of deleted attributes.
848
+ #
849
+ # sandy = Client.find_by_name 'Sandy'
850
+ # sandy.reload
851
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
852
+ # puts sandy.delete_attributes('toys') #=> ['toys']
853
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
854
+ #
855
+ def delete_attributes(*attrs_list)
856
+ raise_on_id_absence
857
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
858
+ attrs_list.delete('id')
859
+ unless attrs_list.blank?
860
+ connection.delete_attributes(domain, id, attrs_list)
861
+ attrs_list.each { |attribute| @attributes.delete(attribute) }
862
+ end
863
+ attrs_list
864
+ end
865
+
866
+ # Delete the Item entirely from SDB.
867
+ #
868
+ # sandy = Client.find_by_name 'Sandy'
869
+ # sandy.reload
870
+ # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
871
+ # puts sandy.delete
872
+ # sandy.reload
873
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
874
+ #
875
+ def delete
876
+ raise_on_id_absence
877
+ connection.delete_attributes(domain, id)
878
+ end
879
+
880
+ # Item ID
881
+ def to_s
882
+ @id
883
+ end
884
+
885
+ # Returns true if this object hasn‘t been saved yet.
886
+ def new_record?
887
+ @new_record
888
+ end
889
+
890
+ def mark_as_old # :nodoc:
891
+ @new_record = false
892
+ end
893
+
894
+ private
895
+
896
+ def raise_on_id_absence
897
+ raise ActiveSdbError.new('Unknown record id') unless id
898
+ end
899
+
900
+ def prepare_for_update
901
+ @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
902
+ end
903
+
904
+ def uniq_values(attributes=nil) # :nodoc:
905
+ attrs = {}
906
+ attributes.each do |attribute, values|
907
+ attribute = attribute.to_s
908
+ attrs[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
909
+ attrs.delete(attribute) if values.blank?
910
+ end
911
+ attrs
912
+ end
913
+ end
914
+ end
915
+ end