revans_right_aws 2.0.1

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