risky 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Kyle Kingsbury
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,58 @@
1
+ Risky
2
+ =====
3
+
4
+ A simple, lightweight object layer for Riak.
5
+
6
+ class User < Risky
7
+ bucket :users
8
+ end
9
+
10
+ User.new('clu', 'fights' => 'for the users').save
11
+
12
+ User['clu']['fights'] #=> 'for the users'
13
+
14
+ Built on top of seancribb's excellent riak-client, Risky provides basic
15
+ infrastructure for designing models with attributes (including defaults and
16
+ casting to/from JSON), validation, lifecycle callbacks, link-walking,
17
+ mapreduce, and more. Modules are available for timestamps, chronologically ordered lists, and basic secondary indexes.
18
+
19
+ Risky does not provide the rich API of Ripple, but it also does not require activesupport. It strives to be understandable, minimal, and modular. Magic is avoided in favor of module composition and a compact API.
20
+
21
+ Risky stores every instance of a model in a given bucket, indexed by key. Objects are stored as JSON hashes.
22
+
23
+ Show me the code!
24
+ -----------------
25
+
26
+ class User < Risky
27
+ include Risky::Indexes
28
+ include Risky::Timestamps
29
+
30
+ bucket :users
31
+
32
+ # Provides user.name instead of user['name']
33
+ value :name
34
+ value :twitter, :default => {}
35
+
36
+ # :class is used to cast times from JSON back into Time objects.
37
+ value :updated_at, :class => Time
38
+ value :created_at, :class => Time
39
+
40
+ # Provides User.by_name. Changing the name stores an object in the
41
+ # users_by_name bucket, with key user.name, linking back to us. A validate
42
+ # function is used to ensure uniqueness before saving.
43
+ index :name, :unique => true
44
+
45
+ # Here, a custom proc returns the key used for the index.
46
+ index :twitter_id, :proc => lambda { |user| user.twitter['id'] }
47
+
48
+ # Provides user.followers, a list of links with the 'followers' tag.
49
+ links :followers
50
+ end
51
+
52
+ License
53
+ -------
54
+
55
+ Risky was developed by Kyle Kingsbury <aphyr@aphyr.com> at http://vodpod.com,
56
+ for their iPad social video app "Showyou". Generous thanks to Sean Cribbs, Mark
57
+ Phillips, the Basho team, and all the other #riak'ers. Released under the MIT
58
+ license.
data/lib/risky.rb ADDED
@@ -0,0 +1,499 @@
1
+ class Risky
2
+ require 'riak'
3
+
4
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
5
+
6
+ # Exceptions
7
+ require 'risky/invalid'
8
+ require 'risky/not_found'
9
+
10
+ # Plugins
11
+ require 'risky/cron_list'
12
+ require 'risky/indexes'
13
+ require 'risky/timestamps'
14
+
15
+ include Enumerable
16
+
17
+ # Get a model by key. Returns nil if not found. You can also pass opts to
18
+ # #reload (e.g. :r, :merge => false).
19
+ def self.[](key, opts = {})
20
+ return nil unless key
21
+
22
+ begin
23
+ new(key).reload(opts)
24
+ rescue Riak::FailedRequest => e
25
+ raise unless e.code.to_i == 404
26
+ nil
27
+ end
28
+ end
29
+
30
+ # Returns all model instances from the bucket. Why yes, this *could* be
31
+ # expensive, Suzy!
32
+ def self.all(opts = {:reload => true})
33
+ bucket.keys(opts).map do |key|
34
+ self[key]
35
+ end
36
+ end
37
+
38
+ # Indicates that this model may be multivalued; in which case .merge should
39
+ # also be defined.
40
+ def self.allow_mult
41
+ unless bucket.props['allow_mult']
42
+ bucket.props = bucket.props.merge('allow_mult' => true)
43
+ end
44
+ end
45
+
46
+ # The Riak::Bucket backing this model.
47
+ # If name is passed, *sets* the bucket name.
48
+ def self.bucket(name = nil)
49
+ if name
50
+ @bucket_name = name.to_s
51
+ end
52
+
53
+ riak.bucket(@bucket_name)
54
+ end
55
+
56
+ # The string name of the bucket used for storing instances of this model.
57
+ def self.bucket_name
58
+ @bucket_name
59
+ end
60
+
61
+ def self.bucket_name=(bucket)
62
+ @bucket_name = name.to_s
63
+ end
64
+
65
+ # Casts data to appropriate types for values.
66
+ def self.cast(data)
67
+ casted = {}
68
+ data.each do |k, v|
69
+ c = @values[k][:class] rescue nil
70
+ casted[k] = begin
71
+ if c == Time
72
+ Time.iso8601(v)
73
+ else
74
+ v
75
+ end
76
+ rescue
77
+ v
78
+ end
79
+ end
80
+ casted
81
+ end
82
+
83
+ # Counts the number of models via MR
84
+ def self.count
85
+ map("
86
+ function(v) {
87
+ return [1];
88
+ };
89
+ ").reduce("
90
+ function(counts, arg) {
91
+ return [
92
+ counts.reduce(
93
+ function(acc, value) {
94
+ return acc + value
95
+ }, 0)
96
+ ];
97
+ }
98
+ ", :keep => true).run.first
99
+ end
100
+
101
+ # Returns true when record deleted.
102
+ # Returns nil when record was not present to begin with.
103
+ def self.delete(key, opts = {})
104
+ return if key.nil?
105
+ (bucket.delete(key.to_s, opts)[:code] == 204) or nil
106
+ end
107
+
108
+ # Iterate over all items using key streaming.
109
+ def self.each
110
+ bucket.keys(:reload => true) do |keys|
111
+ keys.each do |key|
112
+ yield self[key]
113
+ end
114
+ end
115
+ end
116
+
117
+ # Does the given key exist in our bucket?
118
+ def self.exists?(key)
119
+ return if key.nil?
120
+ bucket.exists? key.to_s
121
+ end
122
+
123
+ # Fills in values from a Riak::RObject
124
+ def self.from_riak_object(riak_object)
125
+ return nil if riak_object.nil?
126
+
127
+ n = new.load_riak_object riak_object
128
+
129
+ # Callback
130
+ n.after_load
131
+ n
132
+ end
133
+
134
+ # Gets an existing record or creates one.
135
+ def self.get_or_new(*args)
136
+ self[*args] or new(args.first)
137
+ end
138
+
139
+ # Establishes methods for manipulating a single link with a given tag.
140
+ def self.link(tag)
141
+ tag = tag.to_s
142
+ class_eval "
143
+ def #{tag}
144
+ begin
145
+ @riak_object.links.find do |l|
146
+ l.tag == #{tag.inspect}
147
+ end.key
148
+ rescue NoMethodError
149
+ nil
150
+ end
151
+ end
152
+
153
+ def #{tag}=(link)
154
+ @riak_object.links.reject! do |l|
155
+ l.tag == #{tag.inspect}
156
+ end
157
+ if link
158
+ @riak_object.links << link.to_link(#{tag.inspect})
159
+ end
160
+ end
161
+ "
162
+ end
163
+
164
+ # Establishes methods for manipulating a set of links with a given tag.
165
+ def self.links(tag)
166
+ tag = tag.to_s
167
+ class_eval "
168
+ def #{tag}
169
+ @riak_object.links.select do |l|
170
+ l.tag == #{tag.inspect}
171
+ end.map do |l|
172
+ l.key
173
+ end
174
+ end
175
+
176
+ def add_#{tag}(link)
177
+ @riak_object.links << link.to_link(#{tag.inspect})
178
+ end
179
+
180
+ def remove_#{tag}(link)
181
+ @riak_object.links.delete link.to_link(#{tag.inspect})
182
+ end
183
+
184
+ def clear_#{tag}
185
+ @riak_object.links.delete_if do |l|
186
+ l.tag == #{tag.inspect}
187
+ end
188
+ end
189
+
190
+ def #{tag}_count
191
+ @riak_object.links.select{|l| l.tag == #{tag.inspect}}.length
192
+ end
193
+ "
194
+ end
195
+
196
+ # Mapreduce helper
197
+ def self.map(*args)
198
+ mr.map(*args)
199
+ end
200
+
201
+ # Merges n versions of a record together, for read-repair.
202
+ # Returns the merged record.
203
+ def self.merge(versions)
204
+ versions.first
205
+ end
206
+
207
+ # Begins a mapreduce on this model's bucket.
208
+ # If no keys are given, operates on the entire bucket.
209
+ # If keys are given, operates on those keys first.
210
+ def self.mr(keys = nil)
211
+ mr = Riak::MapReduce.new(riak)
212
+
213
+ if keys
214
+ # Add specific keys
215
+ [*keys].compact.inject(mr) do |mr, key|
216
+ mr.add @bucket_name, key.to_s
217
+ end
218
+ else
219
+ # Add whole bucket
220
+ mr.add @bucket_name
221
+ end
222
+ end
223
+
224
+ # MR helper.
225
+ def self.reduce(*args)
226
+ mr.reduce(*args)
227
+ end
228
+
229
+ # The Riak::Client backing this model class.
230
+ def self.riak
231
+ @riak ||= superclass.riak
232
+ end
233
+
234
+ def self.riak=(client)
235
+ @riak = client
236
+ end
237
+
238
+ # Add a new value to this model. Values aren't necessary; you can
239
+ # use Risky#[], but if you would like to cast values to/from JSON or
240
+ # specify defaults, you may:
241
+ #
242
+ # :default => object (#clone is called for each new instance)
243
+ # :class => Time, Integer, etc. Inferred from default.class if present.
244
+ def self.value(value, opts = {})
245
+ value = value.to_s
246
+
247
+ klass = if opts[:class]
248
+ opts[:class]
249
+ elsif opts.include? :default
250
+ opts[:default].class
251
+ else
252
+ nil
253
+ end
254
+ values[value] = opts.merge(:class => klass)
255
+
256
+ class_eval "
257
+ def #{value}; @values[#{value.inspect}]; end
258
+ def #{value}=(value); @values[#{value.inspect}] = value; end
259
+ "
260
+ end
261
+
262
+ # A list of all values we track.
263
+ def self.values
264
+ @values ||= {}
265
+ end
266
+
267
+
268
+
269
+ attr_accessor :values
270
+ attr_accessor :riak_object
271
+
272
+ # Create a new instance from a key and a list of values.
273
+ #
274
+ # Values will be passed to attr= methods where possible, so you can write
275
+ # def password=(p)
276
+ # self['password'] = md5sum p
277
+ # end
278
+ # User.new('me', :password => 'baggins')
279
+ def initialize(key = nil, values = {})
280
+ super()
281
+
282
+ key = key.to_s unless key.nil?
283
+
284
+ @riak_object ||= Riak::RObject.new(self.class.bucket, key)
285
+ @riak_object.content_type = 'application/javascript'
286
+
287
+ @new = true
288
+ @merged = false
289
+ @values = {}
290
+
291
+ # Load values
292
+ values.each do |k,v|
293
+ begin
294
+ send(k.to_s + '=', v)
295
+ rescue NoMethodError
296
+ self[k] = v
297
+ end
298
+ end
299
+
300
+ # Fill in defults.
301
+ self.class.values.each do |k,v|
302
+ self[k] ||= (v[:default].clone rescue v[:default])
303
+ end
304
+ end
305
+
306
+ # Two models compare === if they are of matching class and key.
307
+ def ===(o)
308
+ o.class == self.class and o.key.to_s == self.key.to_s rescue false
309
+ end
310
+
311
+ # Access the values hash.
312
+ def [](k)
313
+ @values[k]
314
+ end
315
+
316
+ # Access the values hash.
317
+ def []=(k, v)
318
+ @values[k] = v
319
+ end
320
+
321
+ def after_create
322
+ end
323
+
324
+ def after_delete
325
+ end
326
+
327
+ # Called when a riak object is used to populate the instance.
328
+ def after_load
329
+ end
330
+
331
+ def after_save
332
+ end
333
+
334
+ def as_json(opts = {})
335
+ h = @values.merge(:key => key)
336
+ h[:errors] = errors unless errors.empty?
337
+ h
338
+ end
339
+
340
+ # Called before creation and validation
341
+ def before_create
342
+ end
343
+
344
+ # Called before deletion
345
+ def before_delete
346
+ end
347
+
348
+ # Called before saving and before validation
349
+ def before_save
350
+ end
351
+
352
+ # Delete this object in the DB and return self.
353
+ def delete
354
+ before_delete
355
+ @riak_object.delete
356
+ after_delete
357
+
358
+ self
359
+ end
360
+
361
+ # A hash for errors on this object
362
+ def errors
363
+ @errors ||= {}
364
+ end
365
+
366
+ # Replaces values and riak_object with data from riak_object.
367
+ def load_riak_object(riak_object, opts = {:merge => true})
368
+ if opts[:merge] and riak_object.conflict? and siblings = riak_object.siblings
369
+ # Engage conflict resolution mode
370
+ final = self.class.merge(
371
+ siblings.map do |sibling|
372
+ self.class.new.load_riak_object(sibling, :merge => false)
373
+ end
374
+ )
375
+
376
+ # Copy final values to self.
377
+ final.instance_variables.each do |var|
378
+ self.instance_variable_set(var, final.instance_variable_get(var))
379
+ end
380
+
381
+ self.merged = true
382
+ else
383
+ # Not merging
384
+ self.values = self.class.cast(JSON.parse(riak_object.raw_data)) rescue {}
385
+ self.class.values.each do |k, v|
386
+ values[k] ||= (v[:default].clone rescue v[:default])
387
+ end
388
+ self.riak_object = riak_object
389
+ self.new = false
390
+ self.merged = false
391
+ end
392
+
393
+ self
394
+ end
395
+
396
+ def inspect
397
+ "#<#{self.class} #{key} #{@values.inspect}>"
398
+ end
399
+
400
+ def key=(key)
401
+ if key.nil?
402
+ @riak_object.key = nil
403
+ else
404
+ @riak_object.key = key.to_s
405
+ end
406
+ end
407
+
408
+ def key
409
+ @riak_object.key
410
+ end
411
+
412
+ def merged=(merged)
413
+ @merged = !!merged
414
+ end
415
+
416
+ # Has this model been merged from multiple siblings?
417
+ def merged?
418
+ @merged
419
+ end
420
+
421
+ def new=(new)
422
+ @new = !!new
423
+ end
424
+
425
+ # Is this model freshly created; i.e. not saved in the database yet?
426
+ def new?
427
+ @new
428
+ end
429
+
430
+ # Reload this model's data from Riak.
431
+ # opts are passed to Riak::Bucket[]
432
+ def reload(opts = {})
433
+ # Get object from riak.
434
+ riak_object = self.class.bucket[key, opts]
435
+
436
+ # Load
437
+ load_riak_object riak_object
438
+
439
+ # Callback
440
+ after_load
441
+ self
442
+ end
443
+
444
+ # Saves this model.
445
+ #
446
+ # Calls #validate and #valid? unless :validate is false.
447
+ #
448
+ # Converts @values to_json and saves it to riak.
449
+ #
450
+ # :w and :dw are also supported.
451
+ def save(opts = {})
452
+ before_create if @new
453
+ before_save
454
+
455
+ unless opts[:validate] == false
456
+ return false unless valid?
457
+ end
458
+
459
+ @riak_object.raw_data = @values.to_json
460
+
461
+ store_opts = {}
462
+ store_opts[:w] = opts[:w] if opts[:w]
463
+ store_opts[:dw] = opts[:dw] if opts[:dw]
464
+ @riak_object.store store_opts
465
+
466
+ after_create if @new
467
+ after_save
468
+
469
+ @new = false
470
+
471
+ self
472
+ end
473
+
474
+ # This is provided for convenience; #save does *not* use this method, and you
475
+ # are free to override it.
476
+ def to_json(*a)
477
+ as_json.to_json(*a)
478
+ end
479
+
480
+ # Returns a Riak::Link object pointing to this record.
481
+ def to_link(*a)
482
+ @riak_object.to_link(*a)
483
+ end
484
+
485
+ # Calls #validate and checks whether the errors hash is empty.
486
+ def valid?
487
+ @errors = {}
488
+ validate
489
+ @errors.empty?
490
+ end
491
+
492
+ # Determines whether the model is valid. Sets the contents of #errors if
493
+ # invalid.
494
+ def validate
495
+ if key.blank?
496
+ errors[:key] = 'is missing'
497
+ end
498
+ end
499
+ end
@@ -0,0 +1,189 @@
1
+ module Risky::CronList
2
+ # Provides methods which allow a Risky model to act as a chronological list
3
+ # of references to some other model.
4
+
5
+ module ClassMethods
6
+ def item_class(klass = nil)
7
+ if klass
8
+ @item_class = klass
9
+ else
10
+ @item_class or raise "no item class defined for #{self}"
11
+ end
12
+ end
13
+
14
+ def limit(limit = nil)
15
+ if limit
16
+ @limit = limit
17
+ else
18
+ @limit
19
+ end
20
+ end
21
+
22
+ def merge(versions)
23
+ p = super(versions)
24
+ items = sort_items(versions.map(&:items).flatten!.uniq)
25
+
26
+ if limit = self.limit
27
+ p.items = items[0...limit]
28
+ p.removed_items = items[limit..-1] || []
29
+ else
30
+ p.items = items
31
+ end
32
+
33
+ p
34
+ end
35
+
36
+ # Sorts a list of items into the appropriate order.
37
+ def sort_items(items)
38
+ items.sort { |a,b| b <=> a }
39
+ end
40
+ end
41
+
42
+ def self.included(base)
43
+ base.value :items, :default => []
44
+ base.extend ClassMethods
45
+ end
46
+
47
+ def initialize(*a)
48
+ super *a
49
+
50
+ @added_items ||= []
51
+ @removed_items ||= []
52
+ end
53
+
54
+ def <<(item)
55
+ if self.class.item_class === item
56
+ # Item is already an object; ensure it has a key.
57
+ item.key ||= new_item_key(item)
58
+ else
59
+ # Create a new item with <item> as the data.
60
+ item = self.class.item_class.new(new_item_key(item), item)
61
+ end
62
+
63
+ add_item item
64
+
65
+ trim
66
+ end
67
+ alias :add :<<
68
+
69
+ def add_item(item)
70
+ @added_items << item
71
+ @removed_items.delete item.key
72
+
73
+ unless items.include? item.key
74
+ items.unshift item.key
75
+
76
+ if self.class.sort_items(items[0,2]).first != item.key
77
+ # We must reorder.
78
+ self.items = self.class.sort_items(items)
79
+ end
80
+ end
81
+ end
82
+
83
+ def added_items
84
+ @added_items
85
+ end
86
+
87
+ def added_items=(items)
88
+ @added_items = items
89
+ end
90
+
91
+ def after_delete
92
+ super
93
+
94
+ (@removed_items + items).each do |item|
95
+ delete_item(item) rescue nil
96
+ end
97
+
98
+ @removed_items.clear
99
+ end
100
+
101
+ def after_save
102
+ super
103
+
104
+ @removed_items.each do |item|
105
+ delete_item(item) rescue nil
106
+ end
107
+
108
+ @added_items.clear
109
+ @removed_items.clear
110
+ end
111
+
112
+ def all
113
+ items.map { |item| self.class.item_class[item] }
114
+ end
115
+
116
+ def before_save
117
+ super
118
+
119
+ @added_items.each do |item|
120
+ item.save(:w => :all) or raise "unable to save #{item}"
121
+ end
122
+ end
123
+
124
+ # Remove all items. Items will actually be deleted on #save
125
+ def clear
126
+ @removed_items += items
127
+ items.clear
128
+ end
129
+
130
+ # Remove dangling references. Items will actually be deleted on #save
131
+ # TODO...
132
+ #def cleanup
133
+ #end
134
+
135
+ # Remove an item by key.
136
+ def remove(item_key)
137
+ if key = items.delete(item_key)
138
+ # This item existed.
139
+ @added_items.reject! do |item|
140
+ item['key'] == item_key
141
+ end
142
+
143
+ @removed_items << key
144
+ end
145
+
146
+ key
147
+ end
148
+
149
+ # Takes an entry of items and deletes it.
150
+ def delete_item(item)
151
+ self.class.item_class.delete item
152
+ end
153
+
154
+ # Generates a key for a newly added item.
155
+ def new_item_key(item = nil)
156
+ # Re-use existing time
157
+ begin
158
+ t = item['created_at']
159
+ if t.kind_of? String
160
+ time = Time.iso8601(t)
161
+ elsif t.kind_of? Time
162
+ time = t
163
+ end
164
+ rescue
165
+ end
166
+ time ||= Time.now
167
+
168
+ "k#{key}_t#{time.to_f}#{rand(10**5)}"
169
+ end
170
+
171
+ def removed_items
172
+ @removed_items
173
+ end
174
+
175
+ def removed_items=(items)
176
+ @removed_items = items
177
+ end
178
+
179
+ def trim
180
+ # Remove expired items
181
+ if limit = self.class.limit
182
+ if removed = items.slice!(limit..-1)
183
+ @removed_items += removed
184
+ end
185
+ end
186
+
187
+ self
188
+ end
189
+ end
@@ -0,0 +1,118 @@
1
+ module Risky::Indexes
2
+ # Provides indexes on an attribute. Mostly.
3
+
4
+ def self.included(base)
5
+ base.instance_eval do
6
+ @indexes = {}
7
+
8
+ def indexes
9
+ @indexes
10
+ end
11
+
12
+ # Options:
13
+ # :proc => Anything responding to #[record]. Returns the key used for the index.
14
+ # :unique => Perform a unique check to ensure this index does not
15
+ # conflict with another record.
16
+ def index(attribute, opts = {})
17
+ opts[:bucket] ||= "#{@bucket_name}_by_#{attribute}"
18
+ @indexes[attribute] = opts
19
+
20
+ class_eval %{
21
+ def self.by_#{attribute}(value)
22
+ return nil unless value
23
+
24
+ begin
25
+ from_riak_object(
26
+ Riak::RObject.new(
27
+ riak[#{opts[:bucket].inspect}],
28
+ value
29
+ ).walk(:bucket => #{@bucket_name.inspect}).first.first
30
+ )
31
+ rescue Riak::FailedRequest => e
32
+ raise e unless e.code.to_i == 404
33
+ nil
34
+ end
35
+ end
36
+ }
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(*a)
42
+ @old_indexed_values = {}
43
+
44
+ super *a
45
+ end
46
+
47
+ def after_load
48
+ super
49
+
50
+ self.class.indexes.each do |attr, opts|
51
+ @old_indexed_values[attr] = opts[:proc][self] rescue self[attr.to_s]
52
+ end
53
+ end
54
+
55
+ def after_save
56
+ super
57
+
58
+ self.class.indexes.each do |attr, opts|
59
+ current = opts[:proc][self] rescue self[attr.to_s]
60
+ old = @old_indexed_values[attr]
61
+ @old_indexed_values[attr] = current
62
+ unless old == current
63
+ # Remove old index
64
+ if old
65
+ self.class.riak[opts[:bucket]].delete(old) rescue nil
66
+ end
67
+
68
+ # Create new index
69
+ unless current.nil?
70
+ index = Riak::RObject.new(self.class.riak[opts[:bucket]], current)
71
+ index.content_type = 'text/plain'
72
+ index.data = ''
73
+ index.links = Set.new([@riak_robject.to_link('value')])
74
+ index.store
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def before_delete
81
+ super
82
+
83
+ self.class.indexes.each do |attr, opts|
84
+ if key = @old_indexed_values[attr]
85
+ self.class.riak[opts[:bucket]].delete(key) rescue nil
86
+ end
87
+ end
88
+ end
89
+
90
+ def validate
91
+ super
92
+
93
+ # Validate unique indexes
94
+ self.class.indexes.each do |attr, opts|
95
+ next unless opts[:unique]
96
+
97
+ current = opts[:proc][self] rescue self[attr.to_s]
98
+ old = @old_indexed_values[attr]
99
+
100
+ next if current.nil?
101
+ next if current == old
102
+
103
+ # Validate that the record belongs to us.
104
+ begin
105
+ existing = self.class.riak[opts[:bucket]][current]
106
+ existing_key = existing.links.find { |l| l.tag == 'value' }.key
107
+ rescue
108
+ # Any failure here means no current index exists exists.
109
+ next
110
+ end
111
+
112
+ if existing_key and (new? or key != existing_key)
113
+ # Conflicts!
114
+ errors[attr.to_sym] = 'taken'
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,16 @@
1
+ class Risky::Invalid < RuntimeError
2
+ attr_accessor :record
3
+
4
+ def initialize(record, message = nil)
5
+ @record = record
6
+ @message = message || "record invalid"
7
+ end
8
+
9
+ def errors
10
+ @record.errors
11
+ end
12
+
13
+ def to_s
14
+ @message
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ class Risky::NotFound < RuntimeError
2
+ attr_accessor :key
3
+
4
+ def initialize(key, message = nil)
5
+ @record = key
6
+ @message = message || "record not found"
7
+ end
8
+
9
+ def to_s
10
+ @message
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module Risky::Timestamps
2
+ # If created_at and/or updated_at are present, updates them before creation
3
+ # and save, respectively.
4
+
5
+ def before_save
6
+ super
7
+ begin
8
+ self.updated_at = Time.now
9
+ rescue
10
+ end
11
+ end
12
+
13
+ def before_create
14
+ super
15
+
16
+ begin
17
+ self.created_at ||= Time.now
18
+ rescue
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ class Risky
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: risky
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Kyle Kingsbury
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-04-07 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: riak-client
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 8
30
+ - 2
31
+ version: 0.8.2
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description:
35
+ email: aphyr@aphyr.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - lib/risky/invalid.rb
44
+ - lib/risky/not_found.rb
45
+ - lib/risky/version.rb
46
+ - lib/risky/timestamps.rb
47
+ - lib/risky/indexes.rb
48
+ - lib/risky/cron_list.rb
49
+ - lib/risky.rb
50
+ - LICENSE
51
+ - README.markdown
52
+ has_rdoc: true
53
+ homepage: https://github.com/aphyr/risky
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 1
67
+ - 8
68
+ - 6
69
+ version: 1.8.6
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project: risky
80
+ rubygems_version: 1.3.6
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: A Ruby ORM for the Riak distributed database.
84
+ test_files: []
85
+