right_aws 1.6.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -51,8 +51,8 @@ require 'sdb/right_sdb_interface'
51
51
  module RightAws #:nodoc:
52
52
  module VERSION #:nodoc:
53
53
  MAJOR = 1
54
- MINOR = 6
55
- TINY = 2
54
+ MINOR = 7
55
+ TINY = 0
56
56
 
57
57
  STRING = [MAJOR, MINOR, TINY].join('.')
58
58
  end
@@ -0,0 +1,695 @@
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 Alpha 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
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
102
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
103
+ # The +params+ are passed through as-is to RightAws::SdbInterface.new
104
+ def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
105
+ @connection = RightAws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
106
+ end
107
+ end
108
+
109
+ class ActiveSdbError < RuntimeError ; end
110
+
111
+ class << self
112
+ include ActiveSdbConnect
113
+
114
+ # Retreive a list of domains.
115
+ #
116
+ # put RightAws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
117
+ #
118
+ def domains
119
+ connection.list_domains[:domains]
120
+ end
121
+
122
+ # Create new domain.
123
+ # Raises no errors if the domain already exists.
124
+ #
125
+ # RightAws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
126
+ #
127
+ def create_domain(domain_name)
128
+ connection.create_domain(domain_name)
129
+ end
130
+
131
+ # Remove domain from SDB.
132
+ # Raises no errors if the domain does not exist.
133
+ #
134
+ # RightAws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
135
+ #
136
+ def delete_domain(domain_name)
137
+ connection.delete_domain(domain_name)
138
+ end
139
+ end
140
+
141
+ class Base
142
+
143
+ class << self
144
+ include ActiveSdbConnect
145
+
146
+ # next_token value returned by last find: is useful to continue finding
147
+ attr_accessor :next_token
148
+
149
+ # Returns a RightAws::SdbInterface object
150
+ #
151
+ # class A < RightAws::ActiveSdb::Base
152
+ # end
153
+ #
154
+ # class B < RightAws::ActiveSdb::Base
155
+ # end
156
+ #
157
+ # class C < RightAws::ActiveSdb::Base
158
+ # end
159
+ #
160
+ # RightAws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
161
+ #
162
+ # C.establish_connection 'key_id_2', 'secret_key_2'
163
+ #
164
+ # # A and B uses the default connection, C - uses its own
165
+ # puts A.connection #=> #<RightAws::SdbInterface:0xb76d6d7c>
166
+ # puts B.connection #=> #<RightAws::SdbInterface:0xb76d6d7c>
167
+ # puts C.connection #=> #<RightAws::SdbInterface:0xb76d6ca0>
168
+ #
169
+ def connection
170
+ @connection || ActiveSdb::connection
171
+ end
172
+
173
+ @domain = nil
174
+
175
+ # Current domain name.
176
+ #
177
+ # # if 'ActiveSupport' is not loaded then class name converted to downcase
178
+ # class Client < RightAws::ActiveSdb::Base
179
+ # end
180
+ # puts Client.domain #=> 'client'
181
+ #
182
+ # # if 'ActiveSupport' is loaded then class name being tableized
183
+ # require 'activesupport'
184
+ # class Client < RightAws::ActiveSdb::Base
185
+ # end
186
+ # puts Client.domain #=> 'clients'
187
+ #
188
+ # # Explicit domain name definition
189
+ # class Client < RightAws::ActiveSdb::Base
190
+ # set_domain_name :foreign_clients
191
+ # end
192
+ # puts Client.domain #=> 'foreign_clients'
193
+ #
194
+ def domain
195
+ unless @domain
196
+ if defined? ActiveSupport::CoreExtensions::String::Inflections
197
+ @domain = name.tableize
198
+ else
199
+ @domain = name.downcase
200
+ end
201
+ end
202
+ @domain
203
+ end
204
+
205
+ # Change the default domain name to user defined.
206
+ #
207
+ # class Client < RightAws::ActiveSdb::Base
208
+ # set_domain_name :foreign_clients
209
+ # end
210
+ #
211
+ def set_domain_name(domain)
212
+ @domain = domain.to_s
213
+ end
214
+
215
+ # Create domain at SDB.
216
+ # Raises no errors if the domain already exists.
217
+ #
218
+ # class Client < RightAws::ActiveSdb::Base
219
+ # end
220
+ # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
221
+ #
222
+ def create_domain
223
+ connection.create_domain(domain)
224
+ end
225
+
226
+ # Remove domain from SDB.
227
+ # Raises no errors if the domain does not exist.
228
+ #
229
+ # class Client < RightAws::ActiveSdb::Base
230
+ # end
231
+ # Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
232
+ #
233
+ def delete_domain
234
+ connection.delete_domain(domain)
235
+ end
236
+
237
+ # Perform a find request.
238
+ #
239
+ # Single record:
240
+ #
241
+ # Client.find(:first)
242
+ # Client.find(:first, :conditions=> [ "['name'=?] intersection ['wife'=?]", "Jon", "Sandy"])
243
+ #
244
+ # Bunch of records:
245
+ #
246
+ # Client.find(:all)
247
+ # Client.find(:all, :limit => 10)
248
+ # Client.find(:all, :conditions=> [ "['name'=?] intersection ['girlfriend'=?]", "Jon", "Judy"])
249
+ # Client.find(:all, :conditions=> [ "['name'=?]", "Sandy"], :limit => 3)
250
+ #
251
+ # Records by ids:
252
+ #
253
+ # Client.find('1')
254
+ # Client.find('1234987b4583475347523948')
255
+ # Client.find('1','2','3','4', :conditions=> [ "['toys'=?]", "beer"])
256
+ #
257
+ # Find helpers: RightAws::ActiveSdb::Base.find_by_... and RightAws::ActiveSdb::Base.find_all_by_...
258
+ #
259
+ # Client.find_by_name('Matias Rust')
260
+ # Client.find_by_name_and_city('Putin','Moscow')
261
+ # Client.find_by_name_and_city_and_post('Medvedev','Moscow','president')
262
+ #
263
+ # Client.find_all_by_author('G.Bush jr')
264
+ # Client.find_all_by_age_and_gender_and_ethnicity('34','male','russian')
265
+ #
266
+ # Returned records have to be +reloaded+ to access their attributes.
267
+ #
268
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
269
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
270
+ #
271
+ # Continue listing:
272
+ # # initial listing
273
+ # Client.find(:all, :limit => 10)
274
+ # # continue listing
275
+ # begin
276
+ # Client.find(:all, :limit => 10, :next_token => Client.next_token)
277
+ # end while Client.next_token
278
+ #
279
+ def find(*args)
280
+ options = args.last.is_a?(Hash) ? args.pop : {}
281
+ case args.first
282
+ when :all : find_every options
283
+ when :first : find_initial options
284
+ else find_from_ids args, options
285
+ end
286
+ end
287
+
288
+ protected
289
+
290
+ def query(query_expression=nil, max_number_of_items = nil, next_token = nil) # :nodoc:
291
+ @next_token = next_token
292
+ # request items
293
+ query_result = self.connection.query(domain, query_expression, max_number_of_items, @next_token)
294
+ @next_token = query_result[:next_token]
295
+ items = query_result[:items].map do |name|
296
+ new_item = self.new('id' => name)
297
+ new_item.mark_as_old
298
+ new_item
299
+ end
300
+ items
301
+ end
302
+
303
+ def find_every(options) # :nodoc:
304
+ query(options[:conditions], options[:limit], options[:next_token])
305
+ end
306
+
307
+ def find_initial(options) # :nodoc:
308
+ options[:limit] = 1
309
+ find_every(options)[0]
310
+ end
311
+
312
+ def find_from_ids(args, options) # :nodoc:
313
+ cond = []
314
+ # detect amount of records requested
315
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
316
+ # flatten ids
317
+ args = args.to_a.flatten
318
+ args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
319
+ ids_cond = "[#{cond.join(' OR ')}]"
320
+ # user defined :conditions to string (if it was defined)
321
+ if options[:conditions].is_a?(Array)
322
+ options[:conditions] = connection.query_expression_from_array(options[:conditions])
323
+ end
324
+ # join ids condition and user defined conditions
325
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
326
+ result = find_every(options)
327
+ # if one record was requested then return it
328
+ unless bunch_of_records_requested
329
+ result.first
330
+ else
331
+ # if a bunch of records was requested then return check that we found all of them
332
+ # and return as an array
333
+ unless args.size == result.size
334
+ id_list = args.map{|i| "'#{i}'"}.join(',')
335
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
336
+ else
337
+ result
338
+ end
339
+ end
340
+ end
341
+
342
+ # find_by helpers
343
+ def find_all_by_(format_str, args, limit=nil) # :nodoc:
344
+ fields = format_str.to_s.sub(/^find_(all_)?by_/,'').split('_and_')
345
+ conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
346
+ find(:all, :conditions => [conditions, *args], :limit => limit)
347
+ end
348
+
349
+ def find_by_(format_str, args) # :nodoc:
350
+ find_all_by_(format_str, args, 1)[0]
351
+ end
352
+
353
+ def method_missing(method, *args) # :nodoc:
354
+ if method.to_s[/^find_all_by_/] then return find_all_by_(method, args)
355
+ elsif method.to_s[/^find_by_/] then return find_by_(method, args)
356
+ else super(method, *args)
357
+ end
358
+ end
359
+
360
+ end
361
+
362
+ def self.generate_id # :nodoc:
363
+ result = ''
364
+ result = UUID.timestamp_create().to_s
365
+ result
366
+ end
367
+
368
+ public
369
+
370
+ # instance attributes
371
+ attr_accessor :attributes
372
+
373
+ # item name
374
+ attr_accessor :id
375
+
376
+ # Create new Item instance.
377
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
378
+ #
379
+ # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
380
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
381
+ # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
382
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
383
+ #
384
+ def initialize(attrs={})
385
+ @attributes = uniq_values(attrs)
386
+ @new_record = true
387
+ end
388
+
389
+ # Create and save new Item instance.
390
+ # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
391
+ #
392
+ # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
393
+ # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
394
+ #
395
+ def self.create(attributes={})
396
+ item = self.new(attributes)
397
+ item.save
398
+ item
399
+ end
400
+
401
+ # Returns an item id. Same as: item['id'] or item.attributes['id']
402
+ def id
403
+ @attributes['id']
404
+ end
405
+
406
+ # Sets an item id.
407
+ def id=(id)
408
+ @attributes['id'] = id.to_s
409
+ end
410
+
411
+ # Returns a hash of all the attributes.
412
+ #
413
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
414
+ #
415
+ def attributes
416
+ @attributes.dup
417
+ end
418
+
419
+ # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
420
+ # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
421
+ #
422
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
423
+ # # set new attributes ('id' is missed)
424
+ # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
425
+ # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
426
+ # # set new attributes ('id' is set)
427
+ # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
428
+ # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
429
+ #
430
+ def attributes=(attrs)
431
+ old_id = @attributes['id']
432
+ @attributes = uniq_values(attrs)
433
+ @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
434
+ self.attributes
435
+ end
436
+
437
+ def connection
438
+ self.class.connection
439
+ end
440
+
441
+ # Item domain name.
442
+ def domain
443
+ self.class.domain
444
+ end
445
+
446
+ # Returns the values of the attribute identified by +attribute+.
447
+ #
448
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
449
+ #
450
+ def [](attribute)
451
+ @attributes[attribute.to_s]
452
+ end
453
+
454
+ # Updates the attribute identified by +attribute+ with the specified +values+.
455
+ #
456
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
457
+ # item['Cat'] = ["Whiskas", "chicken"]
458
+ # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
459
+ #
460
+ def []=(attribute, values)
461
+ attribute = attribute.to_s
462
+ @attributes[attribute] = attribute == 'id' ? values.to_s : values.to_a.uniq
463
+ end
464
+
465
+ # Reload attributes from SDB. Replaces in-memory attributes.
466
+ #
467
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
468
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
469
+ #
470
+ def reload
471
+ raise_on_id_absence
472
+ old_id = id
473
+ attrs = connection.get_attributes(domain, id)[:attributes]
474
+ @attributes = {}
475
+ unless attrs.blank?
476
+ attrs.each { |attribute, values| @attributes[attribute] = values }
477
+ @attributes['id'] = old_id
478
+ end
479
+ mark_as_old
480
+ @attributes
481
+ end
482
+
483
+ # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
484
+ # +attrs_list+ is an array or comma separated list of attributes names.
485
+ # Returns a hash of loaded attributes.
486
+ #
487
+ # This is not the best method to get a bunch of attributes because
488
+ # a web service call is being performed for every attribute.
489
+ #
490
+ # item = Client.find_by_name('Cat')
491
+ # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
492
+ #
493
+ def reload_attributes(*attrs_list)
494
+ raise_on_id_absence
495
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
496
+ attrs_list.delete('id')
497
+ result = {}
498
+ attrs_list.flatten.uniq.each do |attribute|
499
+ attribute = attribute.to_s
500
+ values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
501
+ unless values.blank?
502
+ @attributes[attribute] = result[attribute] = values
503
+ else
504
+ @attributes.delete(attribute)
505
+ end
506
+ end
507
+ mark_as_old
508
+ result
509
+ end
510
+
511
+ # Stores in-memory attributes to SDB.
512
+ # Adds the attributes values to already stored at SDB.
513
+ # Returns a hash of stored attributes.
514
+ #
515
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
516
+ # sandy['toys'] = 'boys'
517
+ # sandy.put
518
+ # sandy['toys'] = 'patchwork'
519
+ # sandy.put
520
+ # sandy['toys'] = 'kids'
521
+ # sandy.put
522
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
523
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
524
+ #
525
+ # compare to +save+ method
526
+ def put
527
+ @attributes = uniq_values(@attributes)
528
+ prepare_for_update
529
+ attrs = @attributes.dup
530
+ attrs.delete('id')
531
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
532
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
533
+ mark_as_old
534
+ @attributes
535
+ end
536
+
537
+ # Stores specified attributes.
538
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
539
+ # Returns a hash of saved attributes.
540
+ #
541
+ # see to +put+ method
542
+ def put_attributes(attrs)
543
+ attrs = uniq_values(attrs)
544
+ prepare_for_update
545
+ # if 'id' is present in attrs hash:
546
+ # replace internal 'id' attribute and remove it from the attributes to be sent
547
+ @attributes['id'] = attrs['id'] unless attrs['id'].blank?
548
+ attrs.delete('id')
549
+ # add new values to all attributes from list
550
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
551
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
552
+ attrs.each do |attribute, values|
553
+ @attributes[attribute] ||= []
554
+ @attributes[attribute] += values
555
+ @attributes[attribute].uniq!
556
+ end
557
+ mark_as_old
558
+ attributes
559
+ end
560
+
561
+ # Store in-memory attributes to SDB.
562
+ # Replaces the attributes values already stored at SDB by in-memory data.
563
+ # Returns a hash of stored attributes.
564
+ #
565
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
566
+ # sandy['toys'] = 'boys'
567
+ # sandy.put
568
+ # sandy['toys'] = 'patchwork'
569
+ # sandy.put
570
+ # sandy['toys'] = 'kids'
571
+ # sandy.put
572
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
573
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
574
+ #
575
+ # compare to +put+ method
576
+ def save
577
+ @attributes = uniq_values(@attributes)
578
+ prepare_for_update
579
+ connection.put_attributes(domain, id, @attributes, :replace)
580
+ mark_as_old
581
+ @attributes
582
+ end
583
+
584
+ # Replaces the attributes at SDB by the given values.
585
+ # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
586
+ # The other in-memory attributes are not being saved.
587
+ # Returns a hash of stored attributes.
588
+ #
589
+ # see +save+ method
590
+ def save_attributes(attrs)
591
+ prepare_for_update
592
+ attrs = uniq_values(attrs)
593
+ # if 'id' is present in attrs hash then replace internal 'id' attribute
594
+ unless attrs['id'].blank?
595
+ @attributes['id'] = attrs['id']
596
+ else
597
+ attrs['id'] = id
598
+ end
599
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
600
+ attrs.each { |attribute, values| attrs[attribute] = values }
601
+ mark_as_old
602
+ attrs
603
+ end
604
+
605
+ # Remove specified values from corresponding attributes.
606
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
607
+ #
608
+ # sandy = Client.find_by_name 'Sandy'
609
+ # sandy.reload
610
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
611
+ # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
612
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
613
+ #
614
+ def delete_values(attrs)
615
+ raise_on_id_absence
616
+ attrs = uniq_values(attrs)
617
+ attrs.delete('id')
618
+ unless attrs.blank?
619
+ connection.delete_attributes(domain, id, attrs)
620
+ attrs.each { |attribute, values| @attributes[attribute] -= values }
621
+ end
622
+ attrs
623
+ end
624
+
625
+ # Removes specified attributes from the item.
626
+ # +attrs_list+ is an array or comma separated list of attributes names.
627
+ # Returns the list of deleted attributes.
628
+ #
629
+ # sandy = Client.find_by_name 'Sandy'
630
+ # sandy.reload
631
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
632
+ # puts sandy.delete_attributes('toys') #=> ['toys']
633
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
634
+ #
635
+ def delete_attributes(*attrs_list)
636
+ raise_on_id_absence
637
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
638
+ attrs_list.delete('id')
639
+ unless attrs_list.blank?
640
+ connection.delete_attributes(domain, id, attrs_list)
641
+ attrs_list.each { |attribute| @attributes.delete(attribute) }
642
+ end
643
+ attrs_list
644
+ end
645
+
646
+ # Delete the Item entirely from SDB.
647
+ #
648
+ # sandy = Client.find_by_name 'Sandy'
649
+ # sandy.reload
650
+ # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
651
+ # puts sandy.delete
652
+ # sandy.reload
653
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
654
+ #
655
+ def delete
656
+ raise_on_id_absence
657
+ connection.delete_attributes(domain, id)
658
+ end
659
+
660
+ # Item ID
661
+ def to_s
662
+ @id
663
+ end
664
+
665
+ # Returns true if this object hasn‘t been saved yet.
666
+ def new_record?
667
+ @new_record
668
+ end
669
+
670
+ def mark_as_old # :nodoc:
671
+ @new_record = false
672
+ end
673
+
674
+ private
675
+
676
+ def raise_on_id_absence
677
+ raise ActiveSdbError.new('Unknown record id') unless id
678
+ end
679
+
680
+ def prepare_for_update
681
+ @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
682
+ end
683
+
684
+ def uniq_values(attributes=nil) # :nodoc:
685
+ attrs = {}
686
+ attributes.each do |attribute, values|
687
+ attribute = attribute.to_s
688
+ attrs[attribute] = attribute == 'id' ? values.to_s : values.to_a.uniq
689
+ attrs.delete(attribute) if values.blank?
690
+ end
691
+ attrs
692
+ end
693
+ end
694
+ end
695
+ end