risky 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.markdown +58 -0
- data/lib/risky.rb +499 -0
- data/lib/risky/cron_list.rb +189 -0
- data/lib/risky/indexes.rb +118 -0
- data/lib/risky/invalid.rb +16 -0
- data/lib/risky/not_found.rb +12 -0
- data/lib/risky/timestamps.rb +21 -0
- data/lib/risky/version.rb +3 -0
- metadata +85 -0
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,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
|
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
|
+
|