hackerdude-aws 2.3.25

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,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