ahoward-helene 0.0.3

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 (72) hide show
  1. data/Rakefile +274 -0
  2. data/helene.gemspec +26 -0
  3. data/lib/helene.rb +113 -0
  4. data/lib/helene/attempt.rb +46 -0
  5. data/lib/helene/aws.rb +50 -0
  6. data/lib/helene/config.rb +147 -0
  7. data/lib/helene/content_type.rb +15 -0
  8. data/lib/helene/content_type.yml +661 -0
  9. data/lib/helene/error.rb +12 -0
  10. data/lib/helene/logging.rb +55 -0
  11. data/lib/helene/objectpool.rb +220 -0
  12. data/lib/helene/rails.rb +21 -0
  13. data/lib/helene/rightscale/acf/right_acf_interface.rb +379 -0
  14. data/lib/helene/rightscale/awsbase/benchmark_fix.rb +39 -0
  15. data/lib/helene/rightscale/awsbase/right_awsbase.rb +803 -0
  16. data/lib/helene/rightscale/awsbase/support.rb +111 -0
  17. data/lib/helene/rightscale/ec2/right_ec2.rb +1737 -0
  18. data/lib/helene/rightscale/net_fix.rb +160 -0
  19. data/lib/helene/rightscale/right_aws.rb +71 -0
  20. data/lib/helene/rightscale/right_http_connection.rb +507 -0
  21. data/lib/helene/rightscale/s3/right_s3.rb +1094 -0
  22. data/lib/helene/rightscale/s3/right_s3_interface.rb +1180 -0
  23. data/lib/helene/rightscale/sdb/active_sdb.rb +930 -0
  24. data/lib/helene/rightscale/sdb/right_sdb_interface.rb +696 -0
  25. data/lib/helene/rightscale/sqs/right_sqs.rb +388 -0
  26. data/lib/helene/rightscale/sqs/right_sqs_gen2.rb +286 -0
  27. data/lib/helene/rightscale/sqs/right_sqs_gen2_interface.rb +444 -0
  28. data/lib/helene/rightscale/sqs/right_sqs_interface.rb +596 -0
  29. data/lib/helene/s3.rb +34 -0
  30. data/lib/helene/s3/bucket.rb +379 -0
  31. data/lib/helene/s3/grantee.rb +134 -0
  32. data/lib/helene/s3/key.rb +162 -0
  33. data/lib/helene/s3/owner.rb +16 -0
  34. data/lib/helene/sdb.rb +9 -0
  35. data/lib/helene/sdb/base.rb +1204 -0
  36. data/lib/helene/sdb/base/associations.rb +481 -0
  37. data/lib/helene/sdb/base/attributes.rb +90 -0
  38. data/lib/helene/sdb/base/connection.rb +20 -0
  39. data/lib/helene/sdb/base/error.rb +20 -0
  40. data/lib/helene/sdb/base/hooks.rb +82 -0
  41. data/lib/helene/sdb/base/literal.rb +52 -0
  42. data/lib/helene/sdb/base/logging.rb +23 -0
  43. data/lib/helene/sdb/base/transactions.rb +53 -0
  44. data/lib/helene/sdb/base/type.rb +137 -0
  45. data/lib/helene/sdb/base/types.rb +123 -0
  46. data/lib/helene/sdb/base/validations.rb +256 -0
  47. data/lib/helene/sdb/cast.rb +114 -0
  48. data/lib/helene/sdb/connection.rb +36 -0
  49. data/lib/helene/sdb/error.rb +5 -0
  50. data/lib/helene/sdb/interface.rb +412 -0
  51. data/lib/helene/sdb/sentinel.rb +15 -0
  52. data/lib/helene/sleepcycle.rb +29 -0
  53. data/lib/helene/superhash.rb +297 -0
  54. data/lib/helene/util.rb +132 -0
  55. data/test/auth.rb +31 -0
  56. data/test/helper.rb +98 -0
  57. data/test/integration/begin.rb +0 -0
  58. data/test/integration/ensure.rb +8 -0
  59. data/test/integration/s3/bucket.rb +106 -0
  60. data/test/integration/sdb/associations.rb +45 -0
  61. data/test/integration/sdb/creating.rb +13 -0
  62. data/test/integration/sdb/emptiness.rb +56 -0
  63. data/test/integration/sdb/hooks.rb +19 -0
  64. data/test/integration/sdb/limits.rb +27 -0
  65. data/test/integration/sdb/saving.rb +21 -0
  66. data/test/integration/sdb/selecting.rb +39 -0
  67. data/test/integration/sdb/types.rb +31 -0
  68. data/test/integration/sdb/validations.rb +60 -0
  69. data/test/integration/setup.rb +27 -0
  70. data/test/integration/teardown.rb +21 -0
  71. data/test/loader.rb +39 -0
  72. metadata +139 -0
@@ -0,0 +1,162 @@
1
+ module Helene
2
+ module S3
3
+ class Key
4
+ def url(*args)
5
+ options = args.extract_options!.to_options!
6
+ options.to_options!
7
+ expires = options.delete(:expires) || 24.hours
8
+ headers = options.delete(:headers) || {}
9
+ case args.shift.to_s
10
+ when '', 'get'
11
+ bucket.interface.get_link(bucket, name.to_s, expires, headers)
12
+ end
13
+ end
14
+
15
+ alias_method 'url_for', 'url'
16
+ attr_reader :bucket, :name, :last_modified, :e_tag, :size, :storage_class, :owner
17
+ attr_accessor :headers, :meta_headers
18
+ attr_writer :data
19
+
20
+ def self.split_meta(headers) #:nodoc:
21
+ hash = headers.dup
22
+ meta = {}
23
+ hash.each do |key, value|
24
+ if key[%r/^x-amz-meta-/]
25
+ meta[key.gsub('x-amz-meta-', '')] = value
26
+ hash.delete(key)
27
+ end
28
+ end
29
+ [hash, meta]
30
+ end
31
+
32
+ def self.add_meta_prefix(meta_headers, prefix='x-amz-meta-')
33
+ meta = {}
34
+ meta_headers.each do |meta_header, value|
35
+ if meta_header[/#{prefix}/]
36
+ meta[meta_header] = value
37
+ else
38
+ meta["x-amz-meta-#{meta_header}"] = value
39
+ end
40
+ end
41
+ meta
42
+ end
43
+
44
+ def self.create(bucket, name, data=nil, meta_headers={})
45
+ new(bucket, name, data, {}, meta_headers)
46
+ end
47
+
48
+ def initialize(bucket, name, data=nil, headers={}, meta_headers={},
49
+ last_modified=nil, e_tag=nil, size=nil, storage_class=nil, owner=nil)
50
+ raise 'Bucket must be a Bucket instance.' unless bucket.is_a?(Bucket)
51
+ @bucket = bucket
52
+ @name = name
53
+ @data = data
54
+ @e_tag = e_tag
55
+ @size = size.to_i
56
+ @storage_class = storage_class
57
+ @owner = owner
58
+ @last_modified = last_modified
59
+ if @last_modified && !@last_modified.is_a?(Time)
60
+ @last_modified = Time.parse(@last_modified)
61
+ end
62
+ @headers, @meta_headers = self.class.split_meta(headers)
63
+ @meta_headers.merge!(meta_headers)
64
+ end
65
+
66
+ def to_s
67
+ @name.to_s
68
+ end
69
+
70
+ def full_name(separator='/')
71
+ "#{@bucket.to_s}#{separator}#{@name}"
72
+ end
73
+
74
+ def public_link
75
+ params = @bucket.interface.params
76
+ "#{params[:protocol]}://#{params[:server]}:#{params[:port]}/#{full_name('/')}"
77
+ end
78
+
79
+ def data
80
+ get if !@data and exists?
81
+ @data
82
+ end
83
+
84
+ def get(headers={})
85
+ response = @bucket.interface.get(@bucket.name, @name, headers)
86
+ @data = response[:object]
87
+ @headers, @meta_headers = self.class.split_meta(response[:headers])
88
+ refresh(false)
89
+ @data
90
+ end
91
+
92
+ def put(data=nil, perms=nil, headers={})
93
+ headers['x-amz-acl'] = perms if perms
94
+ @data = data || @data
95
+ meta = self.class.add_meta_prefix(@meta_headers)
96
+ @bucket.interface.put(@bucket.name, @name, @data, meta.merge(headers))
97
+ end
98
+
99
+ def rename(new_name)
100
+ @bucket.interface.rename(@bucket.name, @name, new_name)
101
+ @name = new_name
102
+ end
103
+
104
+ def copy(new_key_or_name)
105
+ new_key_or_name = Key.create(@bucket, new_key_or_name.to_s) unless new_key_or_name.is_a?(Key)
106
+ @bucket.interface.copy(@bucket.name, @name, new_key_or_name.bucket.name, new_key_or_name.name)
107
+ new_key_or_name
108
+ end
109
+
110
+ def move(new_key_or_name)
111
+ new_key_or_name = Key.create(@bucket, new_key_or_name.to_s) unless new_key_or_name.is_a?(Key)
112
+ @bucket.interface.move(@bucket.name, @name, new_key_or_name.bucket.name, new_key_or_name.name)
113
+ new_key_or_name
114
+ end
115
+
116
+ def refresh(head=true)
117
+ new_key = @bucket.find_or_create_key_by_absolute_path(name)
118
+ @last_modified = new_key.last_modified
119
+ @e_tag = new_key.e_tag
120
+ @size = new_key.size
121
+ @storage_class = new_key.storage_class
122
+ @owner = new_key.owner
123
+ if @last_modified
124
+ self.head
125
+ true
126
+ else
127
+ @headers = @meta_headers = {}
128
+ false
129
+ end
130
+ end
131
+
132
+ def head
133
+ @headers, @meta_headers = self.class.split_meta(@bucket.interface.head(@bucket, @name))
134
+ true
135
+ end
136
+
137
+ def reload_meta
138
+ @meta_headers = self.class.split_meta(@bucket.interface.head(@bucket, @name)).last
139
+ end
140
+
141
+ def save_meta(meta_headers)
142
+ meta = self.class.add_meta_prefix(meta_headers)
143
+ @bucket.interface.copy(@bucket.name, @name, @bucket.name, @name, :replace, meta)
144
+ @meta_headers = self.class.split_meta(meta)[1]
145
+ end
146
+
147
+ def exists?
148
+ @bucket.find_or_create_key_by_absolute_path(name).last_modified ? true : false
149
+ end
150
+
151
+
152
+ def delete
153
+ raise 'Key name must be specified.' if @name.blank?
154
+ @bucket.interface.delete(@bucket, @name)
155
+ end
156
+
157
+ def grantees
158
+ Grantee::grantees(self)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,16 @@
1
+ module Helene
2
+ module S3
3
+ class Owner
4
+ attr_reader :id, :name
5
+
6
+ def initialize(id, name)
7
+ @id = id
8
+ @name = name
9
+ end
10
+
11
+ def to_s
12
+ @name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module Helene
2
+ module Sdb
3
+ load 'helene/sdb/error.rb'
4
+ load 'helene/sdb/sentinel.rb'
5
+ load 'helene/sdb/interface.rb'
6
+ load 'helene/sdb/connection.rb'
7
+ load 'helene/sdb/base.rb'
8
+ end
9
+ end
@@ -0,0 +1,1204 @@
1
+
2
+ module Helene
3
+ module Sdb
4
+ class Base
5
+ load 'helene/sdb/base/error.rb'
6
+ load 'helene/sdb/base/logging.rb'
7
+ load 'helene/sdb/base/connection.rb'
8
+ load 'helene/sdb/base/type.rb'
9
+ load 'helene/sdb/base/types.rb'
10
+ load 'helene/sdb/base/literal.rb'
11
+ load 'helene/sdb/base/validations.rb'
12
+ load 'helene/sdb/base/attributes.rb'
13
+ load 'helene/sdb/base/associations.rb'
14
+ load 'helene/sdb/base/transactions.rb'
15
+ load 'helene/sdb/base/hooks.rb'
16
+
17
+ include Attempt
18
+
19
+ class << Base
20
+ # track children
21
+ #
22
+ def subclasses
23
+ @subclasses ||= Array.fields
24
+ end
25
+
26
+ def inherited(subclass)
27
+ super
28
+ ensure
29
+ # TODO - use class_inherited_array - etc
30
+ subclass.domain = domain unless self==Base
31
+ subclass.perform_virtual_consistency = perform_virtual_consistency
32
+ subclass.hooks = hooks.dup
33
+ key = subclass.name.blank? ? subclass.inspect : subclass.name
34
+ subclasses[key] = subclass
35
+ end
36
+
37
+ def superclasses
38
+ @superclasses ||= ancestors.select{|ancestor| ancestor <= Base and ancestor > self}
39
+ end
40
+
41
+ def superclass
42
+ @superclass ||= superclasses.first
43
+ end
44
+
45
+ # virtual consistency
46
+ #
47
+ def perform_virtual_consistency(*value)
48
+ @perform_virtual_consistency = true unless defined?(@perform_virtual_consistency)
49
+ @perform_virtual_consistency = !!value.first unless value.empty?
50
+ @perform_virtual_consistency
51
+ end
52
+
53
+ def perform_virtual_consistency?()
54
+ perform_virtual_consistency()
55
+ end
56
+
57
+ def perform_virtual_consistency=(value)
58
+ perform_virtual_consistency(value)
59
+ end
60
+
61
+ def perform_virtual_consistency!()
62
+ perform_virtual_consistency(true)
63
+ end
64
+
65
+ # domain/migration methods
66
+ #
67
+ def domains
68
+ connection.list_domains[:domains]
69
+ end
70
+
71
+ def domain(*value)
72
+ if value.empty?
73
+ @domain ||= name.tableize.gsub(%r|/|, '--')
74
+ else
75
+ @domain = value.to_s
76
+ end
77
+ while @domain.size < 3
78
+ @domain = "#{ @domain }_"
79
+ end
80
+ @domain
81
+ end
82
+
83
+ def domain=(value)
84
+ domain(value)
85
+ end
86
+
87
+ def set_domain_name(value)
88
+ domain(value)
89
+ end
90
+
91
+ def create_domain(*domains)
92
+ domains.flatten!
93
+ domains.compact!
94
+ domains.push(domain) if domains.blank?
95
+ domains.each do |domain|
96
+ connection.create_domain(domain)
97
+ end
98
+ end
99
+
100
+ def delete_domain(*domains)
101
+ domains.flatten!
102
+ domains.compact!
103
+ domains.push(domain) if domains.blank?
104
+ domains.each do |domain|
105
+ connection.delete_domain(domain)
106
+ end
107
+ end
108
+
109
+ def delete_all_domains!(*domains)
110
+ domains.flatten!
111
+ domains.compact!
112
+ domains.push(self.domains) if domains.blank?
113
+ domains.each do |domain|
114
+ connection.delete_domain(domain)
115
+ end
116
+ end
117
+
118
+ def delete_all
119
+ delete_domain
120
+ create_domain
121
+ end
122
+
123
+ def migrate
124
+ create_domain
125
+ end
126
+
127
+ def migrate!
128
+ delete_domain rescue nil
129
+ create_domain
130
+ end
131
+
132
+ def migration
133
+ m = Module.new{ }
134
+ base = self
135
+ sc =
136
+ class << m; self; end
137
+ sc.module_eval do
138
+ define_method(:up){ base.migrate }
139
+ define_method(:down){ base.delete_domain }
140
+ end
141
+ m
142
+ end
143
+
144
+ # id methods
145
+ #
146
+ def generate_uuid
147
+ Util.uuid
148
+ end
149
+
150
+ def generate_id
151
+ generate_uuid
152
+ end
153
+
154
+ def singular
155
+ name.singularize.downcase
156
+ end
157
+
158
+ def plural
159
+ name.pluralize.downcase
160
+ end
161
+
162
+ # create
163
+ #
164
+ def create(attributes={})
165
+ record = new(attributes)
166
+ record.before_create
167
+ record.save
168
+ record.after_create
169
+ record
170
+ end
171
+
172
+ def create!(attributes={})
173
+ record = new(attributes)
174
+ record.before_create
175
+ record.save!
176
+ record.after_create
177
+ record
178
+ end
179
+
180
+ # batch create/update
181
+ #
182
+ def save_without_validation(*records)
183
+ prepare_for_update
184
+ sdb_attributes = ruby_to_sdb
185
+ connection.put_attributes(domain, id, sdb_attributes, :replace)
186
+ virtually_load(sdb_attributes)
187
+ mark_as_old!
188
+ errors.empty?
189
+ end
190
+
191
+ def batch_put(*args)
192
+ args.flatten!
193
+ options = args.extract_options!.to_options!
194
+ replace = options[:replace]
195
+ records = args.compact
196
+ to_put = []
197
+
198
+ records.each do |record|
199
+ record.prepare_for_update
200
+ item_name = record.id
201
+ sdb_attributes = record.ruby_to_sdb
202
+ to_put.push [item_name, sdb_attributes]
203
+ end
204
+
205
+ results =
206
+ =begin
207
+ to_put.threadify(2, :each_slice, 25) do |slice|
208
+ items = Hash[*slice.to_a.flatten]
209
+ connection.batch_put_attributes(domain, items, options.update(:replace => replace))
210
+ end.flatten
211
+ =end
212
+ results = []
213
+ to_put.each_slice(25) do |slice|
214
+ items = Hash[*slice.to_a.flatten]
215
+ results << connection.batch_put_attributes(domain, items, options.update(:replace => replace))
216
+ end
217
+ results.flatten!
218
+
219
+ records.each do |record|
220
+ record.virtually_load(record.ruby_to_sdb)
221
+ record.mark_as_old!
222
+ end
223
+
224
+ records
225
+ end
226
+
227
+ def batch_save(*args)
228
+ args.flatten!
229
+ options = args.extract_options!.to_options!
230
+ options[:replace] = true
231
+ args.push options
232
+ batch_put(*args)
233
+ end
234
+
235
+ def batch_create(n, options = {}, &block)
236
+ records = nil
237
+ Integer(n).times do |i|
238
+ record = new(options)
239
+ if block
240
+ block.arity == 1 ? block.call(record) : block.call(record, i)
241
+ end
242
+ end
243
+ batch_put(record)
244
+ end
245
+
246
+ def batch_delete(*args)
247
+ args.flatten!
248
+ options = args.extract_options!.to_options!
249
+ records = args.compact
250
+ records.each{|record| record.delete}
251
+ args = records
252
+ args.push options
253
+ batch_put(*args)
254
+ end
255
+
256
+ # prepare attributes from sdb for ruby
257
+ #
258
+ def sdb_to_ruby(attributes = {})
259
+ returning Hash.new do |hash|
260
+ attributes.each do |key, value|
261
+ unless value.nil?
262
+ type = type_for(key)
263
+ value = type ? type.sdb_to_ruby(value) : Type.sdb_to_ruby(value)
264
+ hash[key.to_s] = value
265
+ else
266
+ hash[key.to_s] = nil
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ # prepare attributes from ruby for sdb
273
+ #
274
+ def ruby_to_sdb(attributes = {})
275
+ returning Hash.new do |hash|
276
+ attributes.each do |key, value|
277
+ unless value.nil?
278
+ type = type_for(key)
279
+ value = type ? type.ruby_to_sdb(value) : Type.ruby_to_sdb(value)
280
+ hash[key.to_s] = value
281
+ else
282
+ hash[key.to_s] = nil
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ def type_for name
289
+ attribute = attributes.detect{|attribute| attribute.name == name.to_s}
290
+ attribute.type if attribute
291
+ end
292
+
293
+ # create an existing record
294
+ #
295
+ def old(id, attributes = {})
296
+ attributes = Attributes.for(attributes)
297
+ attributes[:old] = true
298
+ class_for(attributes).new(id, attributes)
299
+ end
300
+
301
+ def class_for(attributes)
302
+ if sti_attribute
303
+ classname = [ attributes[sti_attribute.name.to_s] ].flatten.first
304
+ begin
305
+ classname.blank? ? self : classname.constantize
306
+ rescue NameError => e
307
+ self
308
+ end
309
+ else
310
+ self
311
+ end
312
+ end
313
+
314
+ def sti_attribute
315
+ @sti_attribute ||= attributes.detect{|attribute| attribute.type.sti?}
316
+ end
317
+
318
+ # select/find support
319
+ #
320
+ attr_accessor :next_token
321
+
322
+ def select(*args, &block)
323
+ execute_select(*args, &block)
324
+ end
325
+ alias_method 'find', 'select'
326
+
327
+ def method_missing(message, *args, &block)
328
+ message = message.to_s
329
+ re = %r/^(?:find|select)(_all)?_by_(.*)$/io
330
+ match, all, clause = message.match(re).to_a
331
+ super unless match
332
+ clauses = clause.split(%r/_and_/io)
333
+ conditions = clauses.inject(Hash.new){|hash,attr| hash.update attr => args.shift}
334
+ select(all ? :all : :first, :conditions => conditions)
335
+ end
336
+
337
+ def execute_select(*args, &block)
338
+ log(:debug){ "execute_select <- #{ args.inspect }" }
339
+ options = args.extract_options!.to_options!
340
+
341
+ # yank out specical options used for recurion, formatting, and
342
+ # following huge queries. none of these affect sql generation
343
+ #
344
+ accum = options.delete(:accum) || OpenStruct.new(:items => [], :count => 0)
345
+ raw = options.delete(:raw)
346
+ @next_token = options.delete(:next_token)
347
+
348
+ # handle limts > amazon's threshold specially - we'll be batching them
349
+ #
350
+ limit = Integer(options[:limit]) if options.has_key?(:limit)
351
+ if limit and limit > 2500
352
+ options[:limit] = 2500
353
+ end
354
+
355
+ # detect the arity of the result set, also set implied limit (:first)
356
+ # and go ahead and recurse for queries that are large sets of ids
357
+ #
358
+ case args.first.to_s
359
+ when "", "all"
360
+ result_arity = -1
361
+ wants = :all
362
+ when "first"
363
+ limit = 1
364
+ result_arity = 1
365
+ wants = :first
366
+ else
367
+ ids = args.flatten.compact
368
+ raise ArgumentError, 'no ids' if ids.blank?
369
+ if(args.first.is_a?(Array) or ids.size > 1)
370
+ result_arity = -1
371
+ wants = :ids
372
+ else
373
+ result_arity = 1
374
+ wants = :id
375
+ end
376
+ if ids.size > 20
377
+ if block
378
+ ids.each_slice(20){|slice| execute_select(*[slice, options], &block)}
379
+ else
380
+ # records = ids.threadify(2, :each_slice, 20){|slice| execute_select(*[slice, options])}.flatten
381
+ records = []
382
+ ids.each_slice(20){|slice| records.push execute_select(*[slice, options])}
383
+ records.flatten!
384
+ return(limit ? records[0,limit] : records)
385
+ end
386
+ end
387
+ end
388
+
389
+ # generate the sql and get the results
390
+ #
391
+ sql = sql_for_select(*[args.dup, options.dup].flatten, &block)
392
+ log(:debug){ "execute_select -> #{ sql.inspect }" }
393
+ result = connection.select(sql, @next_token)
394
+ @next_token = result[:next_token]
395
+ items = result[:items]
396
+
397
+
398
+ # unpack the results into models or hashes (iff :raw=>true). iterate
399
+ # if a block was given while doing so to prevent creating un-needed
400
+ # objects
401
+ #
402
+ result[:items].each do |hash|
403
+ item =
404
+ unless raw
405
+ id, attributes = hash.shift
406
+ old(id, attributes)
407
+ else
408
+ hash
409
+ end
410
+ block ? block.call(item) : accum.items.push(item)
411
+ accum.count += 1
412
+ break if limit and accum.count >= limit
413
+ end
414
+
415
+ # if a next_token was returned handle the recursion/following
416
+ # transparently for the user. handle the specical case where amazon
417
+ # says there are 'more records' even though our client limit has been
418
+ # reached
419
+ #
420
+ if @next_token
421
+ if limit.nil?
422
+ recurse = [
423
+ args,
424
+ options.merge(:next_token => @next_token, :raw => raw, :accum => accum)
425
+ ].flatten
426
+ execute_select(*recurse, &block)
427
+ else
428
+ if accum.count < limit
429
+ recurse = [
430
+ args,
431
+ options.merge(:next_token => @next_token, :raw => raw, :accum => accum, :limit => (limit - accum.count))
432
+ ].flatten
433
+ execute_select(*recurse, &block)
434
+ end
435
+ end
436
+ end
437
+
438
+ # finally, build the return value based on arity and limit (expecting
439
+ # a single result or many or none when iterating)
440
+ #
441
+ if block
442
+ accum.count
443
+ else
444
+ if result_arity == 1
445
+ record = accum.items.first
446
+ raise RecordNotFound if(record.nil? and wants==:id)
447
+ record
448
+ else
449
+ limit ? accum.items[0,limit] : accum.items
450
+ end
451
+ end
452
+ end
453
+
454
+ def sql_for_select(*args)
455
+ options = args.extract_options!.to_options!
456
+ args.flatten!
457
+
458
+ # arity
459
+ #
460
+ case args.first.to_s
461
+ when "", "all"
462
+ :all
463
+ when "first"
464
+ options[:limit] = 1
465
+ :first
466
+ else
467
+ options[:ids] = args.flatten.compact
468
+ args.size == 1 ? :id : :ids
469
+ end
470
+
471
+ # do you want to show deleted records?
472
+ #
473
+ want_deleted = options.has_key?(:deleted) ? options.delete(:deleted) : false
474
+
475
+ # build select
476
+ #
477
+ select = sql_select_list_for(options[:select])
478
+
479
+ # build from
480
+ #
481
+ from = options[:domain] || options[:from] || domain
482
+ from = escape_domain(from)
483
+
484
+ # build conditions
485
+ #
486
+ conditions = (options[:conditions] || {})
487
+ conditions.to_options! if conditions.is_a?(Hash)
488
+ conditions = !conditions.blank? ? " WHERE #{ sql_conditions_for(options[:conditions]) }" : ''
489
+
490
+ # build order
491
+ #
492
+ order = !options[:order].blank? ?
493
+ " ORDER BY #{ sort_by, sort_order = sort_options_for(options[:order]); [escape_attribute(sort_by), sort_order].join(' ') }" : ''
494
+
495
+ # build limit
496
+ #
497
+ limit = !options[:limit].blank? ? " LIMIT #{ options[:limit] }" : ''
498
+
499
+ # build ids
500
+ #
501
+ ids = options[:ids] || []
502
+
503
+ # monkey patch conditions
504
+ #
505
+ unless order.blank? # you must have a predicate for any attribute sorted on...
506
+ sort_by, sort_order = sort_options_for(options[:order])
507
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{ escape_attribute(sort_by) } IS NOT NULL)"
508
+ end
509
+ unless ids.blank?
510
+ list = ids.flatten.map{|id| escape(id)}.join(',')
511
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "ItemName() in (#{ list })"
512
+ end
513
+ #conditions << (conditions.blank? ? " WHERE " : " AND ") << "(deleted_at is not null and every(deleted_at) != 'nil')"
514
+ #conditions << (conditions.blank? ? " WHERE " : " AND ") << "(every(deleted_at) = 'nil' or deleted_at is null)"
515
+ if want_deleted
516
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "`deleted_at`!='nil'"
517
+ else
518
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "`deleted_at`='nil'"
519
+ end
520
+
521
+ # sql
522
+ #
523
+ sql = "SELECT #{ select } FROM #{ from } #{ conditions } #{ order } #{ limit }".strip
524
+ end
525
+
526
+ ItemName = Literal.for('ItemName()') unless defined?(ItemName)
527
+ Splat = Literal.for('*') unless defined?(Splat)
528
+
529
+ def sql_select_list_for(*list)
530
+ list = listify list
531
+ list.map!{|attr| attr =~ %r/^\s*id\s*$/io ? ItemName : attr}
532
+ sql = list.map{|attr| escape_attribute(attr)}.join(',')
533
+ sql.blank? ? Splat : sql
534
+ end
535
+
536
+ def sql_conditions_for(conditions)
537
+ sql =
538
+ case conditions
539
+ when Array
540
+ sql_conditions_from_array(conditions)
541
+ when Hash
542
+ sql_conditions_from_hash(conditions)
543
+ else
544
+ conditions.respond_to?(:to_sql) ? conditions.to_sql : conditions.to_s
545
+ end
546
+ end
547
+
548
+ def sql_conditions_from_array(array)
549
+ return '' if array.blank?
550
+ sql = ''
551
+
552
+ case array.first
553
+ when Hash, Array
554
+ until array.blank?
555
+ arg = array.shift
556
+ sql << (
557
+ case arg
558
+ when Hash
559
+ "(#{ sql_conditions_from_hash(arg) })"
560
+ when Array
561
+ "(#{ sql_conditions_from_array(arg) })"
562
+ else
563
+ " #{ arg.to_s } "
564
+ end
565
+ )
566
+ end
567
+ else
568
+ query = array.shift.to_s
569
+ hash = array.shift
570
+ raise WTF unless array.empty?
571
+ raise WTF unless hash.is_a?(Hash)
572
+
573
+ hash.each do |key, val|
574
+ key = key.to_s.to_sym
575
+ sdb_val = to_condition(key, val)
576
+ re = %r/[:@]#{ key }/
577
+ query.gsub! re, sdb_val
578
+ end
579
+ sql << query
580
+ end
581
+
582
+ sql
583
+ end
584
+
585
+ def sql_conditions_from_hash(hash)
586
+ return '' if hash.blank?
587
+ expression = []
588
+ every_re = %r/every\s*\(\s*([^)])\s*\)/io
589
+
590
+ hash.each do |key, value|
591
+ key = key.to_s
592
+
593
+ m = every_re.match(key)
594
+ if m
595
+ key = m[1]
596
+ every = true
597
+ else
598
+ every = false
599
+ end
600
+
601
+ lhs = escape_attribute(key =~ %r/^\s*id\s*$/oi ? ItemName : key)
602
+
603
+ rhs =
604
+ if value.is_a?(Array)
605
+ first = value.first.to_s.strip.downcase.gsub(%r/\s+/, ' ')
606
+ if(first.delete('() ') == 'every')
607
+ every = value.shift
608
+ op = value.first.to_s.strip.downcase.gsub(%r/\s+/, ' ')
609
+ else
610
+ every = false
611
+ op = first
612
+ end
613
+ case op
614
+ when '=', '!=', '>', '>=', '<', '<=', 'like', 'not like'
615
+ list = value[1..-1].flatten.map{|val| to_condition(key, val)}
616
+ "#{ op } #{ list.join(',') }"
617
+ when 'between'
618
+ a, b, *ignored = value[1..-1].flatten.map{|val| to_condition(key, val)}
619
+ "between #{ a } and #{ b }"
620
+ when 'is null'
621
+ 'is null'
622
+ when 'is not null'
623
+ 'is not null'
624
+ else # 'in'
625
+ value.shift if op == 'in'
626
+ list = value.flatten.map{|val| to_condition(key, val)}
627
+ "in (#{ list.join(',') })"
628
+ end
629
+ else
630
+ "= #{ to_condition(key, value) }"
631
+ end
632
+
633
+ lhs = "every(#{ lhs })" if every
634
+ expression << "#{ lhs } #{ rhs }"
635
+ end
636
+ sql = expression.join(' AND ')
637
+ end
638
+
639
+ def escape_value(value)
640
+ return value if Literal?(value)
641
+ case value
642
+ when TrueClass, FalseClass
643
+ escape(value.to_s)
644
+ else
645
+ connection.escape(connection.ruby_to_sdb(value))
646
+ end
647
+ end
648
+ alias_method 'escape', 'escape_value'
649
+
650
+ def escape_attribute(value)
651
+ return value if Literal?(value)
652
+ return value if value =~ %r/^ItemName(?:\(\))?$/io
653
+ "`#{ value.gsub(%r/`/, '``') }`"
654
+ end
655
+
656
+ def escape_domain(value)
657
+ return value if Literal?(value)
658
+ "`#{ value.gsub(%r/`/, '``') }`"
659
+ end
660
+
661
+ def to_condition(attribute, value)
662
+ return value if Literal?(value)
663
+ return value.to_condition() if value.respond_to?(:to_condition)
664
+ type = type_for(attribute)
665
+ value = type ? type.to_condition(value) : value
666
+ escape_value(value)
667
+ end
668
+
669
+ def listify(*list)
670
+ if list.size == 1 and list.first.is_a?(String)
671
+ list.first.strip.split(%r/\s*,\s*/)
672
+ else
673
+ list.flatten!
674
+ list.compact!
675
+ list
676
+ end
677
+ end
678
+
679
+ def sort_options_for(sort)
680
+ return sort.to_sql if sort.respond_to?(:to_sql)
681
+ pair =
682
+ if sort.is_a?(Array)
683
+ raise ArgumentError, "empty sort" if sort.empty?
684
+ sort.push(:asc) if sort.size < 2
685
+ sort.first(2).map{|s| s.to_s}
686
+ else
687
+ sort.to_s[%r/['"]?(\w+)['"]? *(asc|desc)?/io]
688
+ [$1, ($2 || 'asc')]
689
+ end
690
+ [ quoted_attribute(pair.first), pair.last.to_s ]
691
+ end
692
+
693
+ def quoted_attribute attr
694
+ return attr if Literal?(attr)
695
+ if attr =~ %r/^\s*id\s*$/
696
+ ItemName
697
+ else
698
+ Literal(escape_attribute(attr))
699
+ end
700
+ end
701
+
702
+ def [](*ids)
703
+ select(*ids)
704
+ end
705
+
706
+ def reload_if_exists(record)
707
+ record && record.reload
708
+ end
709
+
710
+ def reload_all_records(*list)
711
+ list.flatten.each{|record| reload_if_exists(record)}
712
+ end
713
+
714
+ def first(*args, &block)
715
+ options = args.extract_options!.to_options!
716
+ n = Integer(args.shift || options[:limit] || 1)
717
+ options.to_options!
718
+ options[:limit] = n
719
+ order = options.delete(:order)
720
+ if order
721
+ sort_by, sort_order = sort_options_for(order)
722
+ options[:order] = [sort_by, :asc]
723
+ else
724
+ options[:order] = [:id, :asc]
725
+ end
726
+ list = select(:all, options, &block)
727
+ n == 1 ? list.first : list.first(n)
728
+ end
729
+
730
+ def last(*args, &block)
731
+ options = args.extract_options!.to_options!
732
+ n = Integer(args.shift || options[:limit] || 1)
733
+ options.to_options!
734
+ options[:limit] = n
735
+ order = options.delete(:order)
736
+ if order
737
+ sort_by, sort_order = sort_options_for(order)
738
+ options[:order] = [sort_by, :desc]
739
+ else
740
+ options[:order] = [:id, :desc]
741
+ end
742
+ list = select(:all, options, &block)
743
+ n == 1 ? list.first : list.first(n)
744
+ end
745
+
746
+ def all(*args, &block)
747
+ select(:all, *args, &block)
748
+ end
749
+
750
+ def count(conditions = {}, &block)
751
+ conditions =
752
+ case conditions
753
+ when Hash, NilClass
754
+ conditions || {}
755
+ else
756
+ {:conditions => conditions}
757
+ end
758
+ options = conditions.has_key?(:conditions) ? conditions : {:conditions => conditions}
759
+ options[:select] = Literal('count(*)')
760
+ sql = sql_for_select(options)
761
+ result = connection.select(sql, &block)
762
+ Integer(result[:items].first['Domain']['Count'].first) rescue(raise(Error, result.inspect))
763
+ end
764
+ end
765
+
766
+ attr_accessor 'id'
767
+ attr_accessor 'new_record'
768
+ alias_method 'new_record?', 'new_record'
769
+ alias_method 'new?', 'new_record'
770
+ attr_accessor 'attributes'
771
+ attr_accessor 'attributes_before_sdb_to_ruby'
772
+ alias_method 'item_name', 'id'
773
+ attr_accessor 'deleted'
774
+ alias_method 'deleted?', 'deleted'
775
+
776
+ class Attributes < ::HashWithIndifferentAccess
777
+ def Attributes.for(arg)
778
+ Attributes === arg ? arg : new(arg)
779
+ end
780
+ end
781
+
782
+ # instance methods
783
+ #
784
+ def initialize(*args, &block)
785
+ @args, @block = args, block
786
+ options = @args.extract_options!.to_options!
787
+ @new_record = true
788
+ @new_record = !!!options.delete(:old) if options.has_key?(:old)
789
+ @new_record = !!!options.delete(:new_record) if options.has_key?(:new_record)
790
+ @id = @args.size == 1 ? @args.shift : generate_id
791
+
792
+ if @new_record
793
+ @attributes = Attributes.new
794
+ before_initialize
795
+ klass.attributes.each{|attribute| attribute.initialize_record(self)}
796
+ klass.associations.each{|association| association.initialize_record(self)}
797
+ options.each do |name, value|
798
+ setter = "#{ name }="
799
+ if respond_to?(setter)
800
+ send(setter, value)
801
+ else
802
+ attributes[name.to_s] = value
803
+ end
804
+ end
805
+ @deleted = attributes['deleted_at'] ? true : false
806
+ @removed = false
807
+ after_initialize
808
+ else
809
+ before_load
810
+ @attributes = Attributes.for(options)
811
+ sdb_to_ruby!
812
+ @deleted = attributes['deleted_at'] ? true : false
813
+ @removed = false
814
+ after_load
815
+ end
816
+ end
817
+
818
+ def klass
819
+ self.class
820
+ end
821
+
822
+ def attributes= attributes
823
+ self.attributes.replace attributes
824
+ end
825
+
826
+ def generate_id
827
+ klass.generate_id
828
+ end
829
+
830
+ def generate_id!
831
+ @id = generate_id
832
+ end
833
+
834
+ def mark_as_old!
835
+ self.new_record = false
836
+ end
837
+
838
+ def [](attribute)
839
+ attributes[attribute.to_s]
840
+ end
841
+
842
+ def []=(key, value)
843
+ attributes[key] = value
844
+ end
845
+
846
+ def sdb_to_ruby(attributes = self.attributes)
847
+ klass.sdb_to_ruby(attributes)
848
+ end
849
+
850
+ def sdb_to_ruby!(attributes = self.attributes)
851
+ self.attributes.replace(sdb_to_ruby(attributes))
852
+ end
853
+
854
+ def ruby_to_sdb(attributes = self.attributes)
855
+ klass.ruby_to_sdb(attributes)
856
+ end
857
+
858
+ def ruby_to_sdb!(attributes = self.attributes)
859
+ self.attributes.replace(ruby_to_sdb(attributes))
860
+ end
861
+
862
+ def reload
863
+ check_id!
864
+ record = attempt{ klass.select(id) || try_again! }
865
+ raise Error, "no record for #{ id.inspect } (yet)" unless record
866
+ replace(record)
867
+ self
868
+ end
869
+ alias_method 'reload!', 'reload'
870
+
871
+ def replace other
872
+ self.id = other.id
873
+ self.attributes.replace other.attributes
874
+ end
875
+
876
+ def raw
877
+ klass.select(id, :raw => true)
878
+ end
879
+
880
+ def created_at
881
+ Time.parse(attributes['created_at'].to_s) unless attributes['created_at'].blank?
882
+ end
883
+ def created_at= time
884
+ attributes['created_at'] = time
885
+ end
886
+
887
+ def updated_at
888
+ Time.parse(attributes['updated_at'].to_s) unless attributes['updated_at'].blank?
889
+ end
890
+ def updated_at= time
891
+ attributes['updated_at'] = time
892
+ end
893
+
894
+ def deleted_at
895
+ Time.parse(attributes['deleted_at'].to_s) unless attributes['deleted_at'].blank?
896
+ end
897
+ def deleted_at= time
898
+ attributes['deleted_at'] = time
899
+ end
900
+
901
+ def update(options = {})
902
+ attributes.update(options)
903
+ end
904
+
905
+ def raising_an_error?
906
+ $!
907
+ end
908
+
909
+ def updating(&block)
910
+ return(block.call) if(defined?(@updating) and @updating)
911
+ @updating = true
912
+ prepare_for_update
913
+ before_update
914
+ block.call
915
+ ensure
916
+ @updating = false
917
+ after_update unless raising_an_error?
918
+ end
919
+
920
+ def save_without_validation
921
+ updating do
922
+ sdb_attributes = ruby_to_sdb
923
+ connection.put_attributes(domain, id, sdb_attributes, :replace)
924
+ virtually_load(sdb_attributes)
925
+ mark_as_old!
926
+ self
927
+ end
928
+ end
929
+
930
+ def prepare_for_update
931
+ time = Transaction.time.iso8601(2)
932
+ attributes['updated_at'] = time
933
+ if new_record?
934
+ attributes['created_at'] ||= time
935
+ attributes['deleted_at'] = nil
936
+ attributes['transaction_id'] = Transaction.id
937
+ end
938
+ end
939
+
940
+ def save(options = {})
941
+ options.to_options!
942
+ should_raise = options[:raise]
943
+ before_save
944
+ if(before_validation()==false)
945
+ raise(RecordInvalid) if should_raise
946
+ return false
947
+ end
948
+ unless valid?
949
+ raise(RecordInvalid) if should_raise
950
+ return false
951
+ end
952
+ after_validation()
953
+ saved = save_without_validation
954
+ raise(RecordNotSaved) if should_raise unless saved
955
+ saved
956
+ ensure
957
+ after_save unless raising_an_error?
958
+ end
959
+
960
+ def save!(options = {})
961
+ save(options.to_options.update(:raise => true))
962
+ end
963
+
964
+ def errors!
965
+ raise Validations::Error.new(self, errors.message)
966
+ end
967
+
968
+ def update!(options = {})
969
+ updating do
970
+ attributes.update(options)
971
+ save!
972
+ virtually_save(attributes)
973
+ self
974
+ end
975
+ end
976
+
977
+ alias_method 'update_attributes', 'update!'
978
+
979
+ def put_attributes(attributes)
980
+ updating do
981
+ sdb_attributes = ruby_to_sdb(attributes)
982
+ connection.put_attributes(domain, id, sdb_attributes)
983
+ virtually_put(sdb_attributes)
984
+ self
985
+ end
986
+ end
987
+
988
+ def save_attributes(attributes = self.attributes)
989
+ updating do
990
+ sdb_attributes = ruby_to_sdb(attributes)
991
+ connection.put_attributes(domain, id, sdb_attributes, :replace)
992
+ virtually_save(sdb_attributes)
993
+ self
994
+ end
995
+ end
996
+
997
+ def replace_attributes(attributes = self.attributes)
998
+ updating do
999
+ delete_attributes(self.attributes.keys)
1000
+ save_attributes(attributes)
1001
+ self
1002
+ end
1003
+ end
1004
+
1005
+ def delete_attributes(*args)
1006
+ updating do
1007
+ args.flatten!
1008
+ args.compact!
1009
+ hashes, arrays = args.partition{|arg| arg.is_a?(Hash)}
1010
+ hashes.map!{|hash| stringify(hash)}
1011
+ hashes.each do |hash|
1012
+ raise ArgumentError, hash.inspect if hash.values.any?{|value| value == nil or value == []}
1013
+ end
1014
+ array = stringify(arrays.flatten)
1015
+ unless array.empty?
1016
+ array_as_hash = array.inject({}){|h,k| h.update k => nil}
1017
+ hashes.push(array_as_hash)
1018
+ end
1019
+ hashes.each{|hash|
1020
+ next if hash.empty?
1021
+ connection.delete_attributes(domain, id, hash)
1022
+ virtually_delete(hash)
1023
+ }
1024
+ self
1025
+ end
1026
+ end
1027
+ alias_method 'delete_values', 'delete_attributes'
1028
+
1029
+ def delete_item
1030
+ connection.delete_item(domain, id)
1031
+ self
1032
+ ensure
1033
+ @removed = true
1034
+ end
1035
+ alias_method 'remove!', 'delete_item'
1036
+
1037
+ # TODO - need to consider how for pass the options along to children being
1038
+ # deleted along the way - first pass with @removed/@deleted
1039
+ #
1040
+ def delete(options = {})
1041
+ options.to_options!
1042
+ before_delete
1043
+ if options[:force]||options[:remove]
1044
+ delete_item
1045
+ else
1046
+ attributes['deleted_at'] = Transaction.time
1047
+ save_without_validation
1048
+ end
1049
+ self
1050
+ ensure
1051
+ @deleted = true
1052
+ after_delete unless $!
1053
+ end
1054
+
1055
+ def delete!(options = {})
1056
+ delete(options.to_options.update(:force => true))
1057
+ end
1058
+
1059
+ def destroy(options = {})
1060
+ before_destroy
1061
+ delete(options)
1062
+ ensure
1063
+ after_destroy unless $!
1064
+ end
1065
+
1066
+ # virtual consistency
1067
+ #
1068
+ def perform_virtual_consistency(*value)
1069
+ @perform_virtual_consistency = klass.perform_virtual_consistency unless defined?(@perform_virtual_consistency)
1070
+ @perform_virtual_consistency = !!value.first unless value.empty?
1071
+ @perform_virtual_consistency
1072
+ end
1073
+
1074
+ def perform_virtual_consistency?()
1075
+ perform_virtual_consistency()
1076
+ end
1077
+
1078
+ def perform_virtual_consistency=(value)
1079
+ perform_virtual_consistency(value)
1080
+ end
1081
+
1082
+ def perform_virtual_consistency!()
1083
+ perform_virtual_consistency(true)
1084
+ end
1085
+
1086
+ def virtually_load(sdb_attributes)
1087
+ #return unless perform_virtual_consistency?
1088
+ self.attributes.replace(sdb_to_ruby(sdb_attributes))
1089
+ end
1090
+
1091
+ def virtually_save(ruby_attributes=self.attributes)
1092
+ #return unless perform_virtual_consistency?
1093
+ sdb_attributes = ruby_to_sdb(ruby_attributes)
1094
+ virtually_load(sdb_attributes)
1095
+ end
1096
+
1097
+ def virtually_put(sdb_attributes)
1098
+ #return unless perform_virtual_consistency?
1099
+ a = sdb_attributes
1100
+ b = ruby_to_sdb
1101
+ (a.keys + b.keys).uniq.each do |key|
1102
+ was_virtually_put = a.has_key?(key)
1103
+ if was_virtually_put
1104
+ val = b[key]
1105
+ val = [val] unless val.is_a?(Array)
1106
+ val += a[key]
1107
+ end
1108
+ end
1109
+ virtually_load(b)
1110
+ end
1111
+
1112
+ def virtually_delete(ruby_attributes)
1113
+ #return unless perform_virtual_consistency?
1114
+ ruby_attributes.keys.each do |key|
1115
+ val = ruby_attributes[key]
1116
+ if val.nil?
1117
+ ruby_attributes.delete(key)
1118
+ attributes.delete(key)
1119
+ end
1120
+ end
1121
+
1122
+ current = ruby_to_sdb
1123
+ deleted = ruby_to_sdb(ruby_attributes)
1124
+
1125
+ deleted.each do |key, deleted_val|
1126
+ deleted_val = [ deleted_val ].flatten
1127
+ current_val = [ current[key] ].flatten
1128
+ deleted_val.each{|val| current_val.delete(val)}
1129
+
1130
+ if current[key].is_a?(Array)
1131
+ current[key] = current_val
1132
+ else
1133
+ if current_val.blank?
1134
+ current[key] = nil
1135
+ else
1136
+ current[key] = current_val
1137
+ end
1138
+ end
1139
+ end
1140
+ virtually_load(current)
1141
+ end
1142
+
1143
+
1144
+ def stringify(arg)
1145
+ case arg
1146
+ when Hash
1147
+ hash = {}
1148
+ arg.each{|key, val| hash[stringify(key)] = stringify(val)}
1149
+ hash
1150
+ when Array
1151
+ arg.map{|arg| stringify(arg)}
1152
+ else
1153
+ arg.to_s
1154
+ end
1155
+ end
1156
+
1157
+ def listify(*list)
1158
+ klass.listify(*list)
1159
+ end
1160
+
1161
+ def check_id!
1162
+ raise Error.new('No record id') unless id
1163
+ end
1164
+
1165
+ def to_hash(options = {})
1166
+ options.to_options!
1167
+ depth = options[:depth] || 0
1168
+ if depth == 0
1169
+ attributes.to_hash
1170
+ else
1171
+ raise NotImplementedError
1172
+ end
1173
+ end
1174
+
1175
+ def to_yaml
1176
+ to_hash.to_yaml
1177
+ end
1178
+
1179
+ def to_json
1180
+ to_hash.to_json
1181
+ end
1182
+
1183
+ def to_param
1184
+ id ? id : 'new'
1185
+ end
1186
+
1187
+ def model_name
1188
+ klass.name
1189
+ end
1190
+
1191
+ def domain
1192
+ klass.domain
1193
+ end
1194
+
1195
+ def escape(value)
1196
+ klass.escape(value)
1197
+ end
1198
+
1199
+ def sti_attribute
1200
+ klass.sti_attribute
1201
+ end
1202
+ end
1203
+ end
1204
+ end