right_aws 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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