revans_right_aws 2.0.1

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