cmeiklejohn-aws 2.3.8

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