ahoward-helene 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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