aws 1.10.1

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