gotime_aws 2.5.6

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