hackerdude-aws 2.3.25

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