risky 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -0
- data/README.markdown +8 -4
- data/Rakefile.rb +31 -0
- data/lib/risky.rb +274 -265
- data/lib/risky/gzip.rb +28 -0
- data/lib/risky/indexes.rb +4 -4
- data/lib/risky/inflector.rb +337 -0
- data/lib/risky/list_keys.rb +58 -0
- data/lib/risky/paginated_collection.rb +11 -0
- data/lib/risky/secondary_indexes.rb +196 -0
- data/lib/risky/version.rb +1 -1
- data/risky.gemspec +22 -0
- data/spec/risky/cron_list_spec.rb +52 -0
- data/spec/risky/crud_spec.rb +69 -0
- data/spec/risky/enumerable_spec.rb +45 -0
- data/spec/risky/gzip_spec.rb +73 -0
- data/spec/risky/indexes_spec.rb +34 -0
- data/spec/risky/resolver_spec.rb +55 -0
- data/spec/risky/secondary_indexes_spec.rb +222 -0
- data/spec/risky/threads_spec.rb +57 -0
- data/spec/risky_spec.rb +100 -0
- data/spec/spec_helper.rb +40 -0
- metadata +87 -27
- data/lib/risky/all.rb +0 -4
- data/lib/risky/threadsafe.rb +0 -42
@@ -0,0 +1,196 @@
|
|
1
|
+
module Risky::SecondaryIndexes
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Add a new secondary index to this model.
|
9
|
+
# Default option is :type => :int, can also be :bin
|
10
|
+
# Default option is :multi => false, can also be true
|
11
|
+
# Option :map can be used to map the index to a model (see map_model).
|
12
|
+
# This assumes by default that the index name ends in _id (use :map => true)
|
13
|
+
# If it ends in something else, use :map => '_suffix'
|
14
|
+
def index2i(name, opts = {})
|
15
|
+
name = name.to_s
|
16
|
+
|
17
|
+
opts.replace({:type => :int, :multi => false, :finder => :find}.merge(opts))
|
18
|
+
indexes2i[name] = opts
|
19
|
+
|
20
|
+
class_eval %Q{
|
21
|
+
def #{name}
|
22
|
+
@indexes2i['#{name}']
|
23
|
+
end
|
24
|
+
|
25
|
+
def #{name}=(value)
|
26
|
+
@indexes2i['#{name}'] = value
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
if opts[:map]
|
31
|
+
if opts[:map] === true # assume that it ends in _id
|
32
|
+
model_name = name[0..-4]
|
33
|
+
map_model(model_name, opts)
|
34
|
+
else
|
35
|
+
model_name = name[0..-(opts[:map].length + 1)]
|
36
|
+
map_model(model_name, opts.merge(:suffix => opts[:map]))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# A list of all secondary indexes
|
42
|
+
def indexes2i
|
43
|
+
@indexes2i ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_by_index(index2i, value)
|
47
|
+
index = "#{index2i}_#{indexes2i[index2i.to_s][:type]}"
|
48
|
+
key = bucket.get_index(index, value).first
|
49
|
+
return nil if key.nil?
|
50
|
+
|
51
|
+
find(key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_all_by_index(index2i, value)
|
55
|
+
index = "#{index2i}_#{indexes2i[index2i.to_s][:type]}"
|
56
|
+
keys = bucket.get_index(index, value)
|
57
|
+
|
58
|
+
find_all_by_key(keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_all_keys_by_index(index2i, value)
|
62
|
+
index = "#{index2i}_#{indexes2i[index2i.to_s][:type]}"
|
63
|
+
bucket.get_index(index, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def paginate_by_index(index2i, value, opts = {})
|
67
|
+
keys = paginate_keys_by_index(index2i, value, opts)
|
68
|
+
Risky::PaginatedCollection.new(find_all_by_key(keys), keys)
|
69
|
+
end
|
70
|
+
|
71
|
+
def paginate_keys_by_index(index2i, value, opts = {})
|
72
|
+
opts.reject! { |k, v| v.nil? }
|
73
|
+
|
74
|
+
index = "#{index2i}_#{indexes2i[index2i.to_s][:type]}"
|
75
|
+
bucket.get_index(index, value, opts)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create(key, values = {}, indexes2i = {}, opts = {})
|
79
|
+
obj = new key, values, indexes2i
|
80
|
+
obj.save(opts)
|
81
|
+
end
|
82
|
+
|
83
|
+
# The map_model method is a convenience method to map the model_id to getters and setters.
|
84
|
+
# The assumption is that you have a value or index2i for model_id.
|
85
|
+
# The default suffix is '_id', so map_model :promotion implies that promotion_id is the index2i.
|
86
|
+
#
|
87
|
+
# For example, map_model :promotion will create these three methods
|
88
|
+
# ```ruby
|
89
|
+
# def promotion
|
90
|
+
# @promotion ||= Promotion.find_by_id promotion_id
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# def promotion=(value)
|
94
|
+
# @promotion = promotion
|
95
|
+
# self.promotion_id = value.nil? ? nil : value.id
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# def promotion_id=(value)
|
99
|
+
# @promotion = nil if self.promotion_id != value
|
100
|
+
# indexes2i['promotion_id'] = value
|
101
|
+
# end
|
102
|
+
# ```
|
103
|
+
def map_model(model_name, opts = {})
|
104
|
+
model_name = model_name.to_s
|
105
|
+
class_name = Risky::Inflector.classify(model_name)
|
106
|
+
|
107
|
+
opts.replace({:type => :index2i, :suffix => '_id'}.merge(opts))
|
108
|
+
|
109
|
+
class_eval %Q{
|
110
|
+
def #{model_name}
|
111
|
+
@#{model_name} ||= #{class_name}.#{opts[:finder]} #{model_name}#{opts[:suffix]}
|
112
|
+
end
|
113
|
+
|
114
|
+
def #{model_name}=(value)
|
115
|
+
@#{model_name} = value
|
116
|
+
self.#{model_name}#{opts[:suffix]} = value.nil? ? nil : value.id
|
117
|
+
end
|
118
|
+
|
119
|
+
def #{model_name}_id=(value)
|
120
|
+
@#{model_name} = nil if self.#{model_name}_id != value
|
121
|
+
indexes2i['#{model_name}#{opts[:suffix]}'] = value
|
122
|
+
end
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
### Instance methods
|
129
|
+
def initialize(key = nil, values = {}, indexes2i = {})
|
130
|
+
super((key.nil? ? nil : key.to_s), values)
|
131
|
+
|
132
|
+
# Parse anything not parsed correctly by Yajl (no support for json_create)
|
133
|
+
self.class.values.each do |k,v|
|
134
|
+
if self[k].is_a?(Hash) && self[k]['json_class']
|
135
|
+
klass = Risky::Inflector.constantize(self[k]['json_class'])
|
136
|
+
self[k] = klass.send(:json_create, self[k])
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
@indexes2i = {}
|
141
|
+
|
142
|
+
indexes2i.each do |k,v|
|
143
|
+
send(k.to_s + '=', v)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def save(opts = {})
|
148
|
+
self.class.indexes2i.each do |k, v|
|
149
|
+
raise ArgumentError, "Nil for index #{k} on #{self.class.name}" if (v[:multi] ? @indexes2i[k].nil? : @indexes2i[k].blank?) && !v[:allow_nil]
|
150
|
+
|
151
|
+
case v[:type]
|
152
|
+
when :int
|
153
|
+
@riak_object.indexes["#{k}_int"] = v[:multi] && @indexes2i[k].respond_to?(:map) ? @indexes2i[k].map(&:to_i) : [ @indexes2i[k].to_i ]
|
154
|
+
when :bin
|
155
|
+
@riak_object.indexes["#{k}_bin"] = v[:multi] && @indexes2i[k].respond_to?(:map) ? @indexes2i[k].map(&:to_s) : [ @indexes2i[k].to_s ]
|
156
|
+
else
|
157
|
+
raise TypeError, "Invalid 2i type '#{v[:type]}' for index #{k} on #{self.class.name}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
super(opts)
|
162
|
+
end
|
163
|
+
|
164
|
+
def load_riak_object(riak_object, opts = {:merge => true})
|
165
|
+
super(riak_object, opts)
|
166
|
+
|
167
|
+
# Parse anything not parsed correctly by Yajl (no support for json_create)
|
168
|
+
self.class.values.each do |k,v|
|
169
|
+
if self[k].is_a?(Hash) && self[k]['json_class']
|
170
|
+
klass = Risky::Inflector.constantize(self[k]['json_class'])
|
171
|
+
self[k] = klass.send(:json_create, self[k])
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
self.class.indexes2i.each do |k, v|
|
176
|
+
case v[:type]
|
177
|
+
when :int
|
178
|
+
@indexes2i[k] = v[:multi] ? @riak_object.indexes["#{k}_int"].map(&:to_i) : @riak_object.indexes["#{k}_int"].first.to_i
|
179
|
+
when :bin
|
180
|
+
@indexes2i[k] = v[:multi] ? @riak_object.indexes["#{k}_bin"].map(&:to_s) : @riak_object.indexes["#{k}_bin"].first.to_s
|
181
|
+
else
|
182
|
+
raise TypeError, "Invalid 2i type '#{v[:type]}' for index #{k} on #{self.class.name}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
def indexes2i
|
190
|
+
@indexes2i
|
191
|
+
end
|
192
|
+
|
193
|
+
def inspect
|
194
|
+
"#<#{self.class} #{key} #{@indexes2i.inspect} #{@values.inspect}>"
|
195
|
+
end
|
196
|
+
end
|
data/lib/risky/version.rb
CHANGED
data/risky.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/risky/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Kyle Kingsbury"]
|
6
|
+
gem.email = ["aphyr@aphyr.com"]
|
7
|
+
gem.description = %q{A lightweight Ruby ORM for Riak.}
|
8
|
+
gem.summary = %q{A Ruby ORM for the Riak distributed database.}
|
9
|
+
gem.homepage = "https://github.com/aphyr/risky"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "risky"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Risky::VERSION
|
17
|
+
gem.license = "MIT"
|
18
|
+
|
19
|
+
gem.add_dependency "riak-client", "~> 1.4.0"
|
20
|
+
gem.add_development_dependency "rspec"
|
21
|
+
gem.add_development_dependency "excon"
|
22
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Item < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
|
6
|
+
bucket :risky_items
|
7
|
+
value :v
|
8
|
+
end
|
9
|
+
|
10
|
+
class CronList < Risky
|
11
|
+
include Risky::ListKeys
|
12
|
+
include Risky::CronList
|
13
|
+
|
14
|
+
bucket :risky_cron_list
|
15
|
+
|
16
|
+
item_class Item
|
17
|
+
limit 5
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
describe Risky::CronList do
|
22
|
+
before :all do
|
23
|
+
CronList.delete_all
|
24
|
+
@l = CronList.new 'test'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'stores an item' do
|
28
|
+
@l << {:v => 1}
|
29
|
+
@l.items.first.should be_kind_of String
|
30
|
+
Item[@l.items.first].should be_nil
|
31
|
+
@l.save.should_not be_false
|
32
|
+
|
33
|
+
@l.reload
|
34
|
+
@l.items.size.should == 1
|
35
|
+
@l.items.first.should be_kind_of String
|
36
|
+
i = Item[@l.items.first]
|
37
|
+
i.should be_kind_of Item
|
38
|
+
i.v.should == 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'limits items' do
|
42
|
+
10.times do |i|
|
43
|
+
@l << {:v => i}
|
44
|
+
end
|
45
|
+
@l.save.should_not be_false
|
46
|
+
@l.reload
|
47
|
+
@l.items.size.should == 5
|
48
|
+
@l.items.map { |k|
|
49
|
+
Item[k].v
|
50
|
+
}.sort.should == (5...10).to_a
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Crud < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
|
6
|
+
bucket :risky_crud
|
7
|
+
value :value
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
describe 'CRUD' do
|
12
|
+
it 'can create a new, blank object' do
|
13
|
+
c = Crud.new
|
14
|
+
c.key.should be_nil
|
15
|
+
c.value.should be_nil
|
16
|
+
c.save.should be_false
|
17
|
+
c.errors[:key].should == 'is missing'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can create and save a named object with a value' do
|
21
|
+
# To be serialized in your best Nazgul voice
|
22
|
+
c = Crud.new 'baggins', :value => 'shire!'
|
23
|
+
c.key.should == 'baggins'
|
24
|
+
c.value.should == 'shire!'
|
25
|
+
c.save.should == c
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can read objects' do
|
29
|
+
Crud['mary_poppins'].should be_nil
|
30
|
+
|
31
|
+
# This rspec clone *IS* named after Francis Bacon, after all.
|
32
|
+
c = Crud.new 'superstition', :value => 'confusion of many states'
|
33
|
+
c.save.should_not be_false
|
34
|
+
|
35
|
+
c2 = Crud['superstition']
|
36
|
+
c2.should === c
|
37
|
+
c2.value.should == 'confusion of many states'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'can test for existence' do
|
41
|
+
Crud.exists?('russells_lagrangian_teapot').should be_false
|
42
|
+
Crud.exists?('superstition').should be_true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'deletes an unfetched object' do
|
46
|
+
Crud.delete('mary_poppins').should be_true
|
47
|
+
Crud.delete('superstition').should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'can delete a fetched object' do
|
51
|
+
v = Crud.new('victim').save
|
52
|
+
v.should_not be_false
|
53
|
+
v.delete.should === v
|
54
|
+
Crud.exists?(v).should be_false
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'checks objects equality by keys' do
|
58
|
+
Crud.new('superstition', 'value' => 'witches').should == Crud.new('superstition', 'value' => 'warlocks')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'checks objects equality by object ids when no keys' do
|
62
|
+
object1 = Crud.new(nil, 'value' => 'witches')
|
63
|
+
object2 = Crud.new(nil, 'value' => 'witches')
|
64
|
+
|
65
|
+
object1.should == object1
|
66
|
+
object2.should == object2
|
67
|
+
object1.should_not == object2
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Enum < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
|
6
|
+
bucket :risky_enum
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'Enumerable' do
|
10
|
+
before :all do
|
11
|
+
# Wipe bucket and replace with 3 items
|
12
|
+
Enum.delete_all
|
13
|
+
|
14
|
+
@keys = ['hume', 'locke', 'spinoza']
|
15
|
+
@keys.each do |key|
|
16
|
+
Enum.new(key).save or raise
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can count' do
|
21
|
+
Enum.count.should == 3
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'can list keys' do
|
25
|
+
Enum.keys.should be_kind_of Array
|
26
|
+
Enum.keys do |key|
|
27
|
+
key.should be_kind_of String
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'can iterate' do
|
32
|
+
seen = []
|
33
|
+
Enum.each do |obj|
|
34
|
+
obj.should be_kind_of Enum
|
35
|
+
seen << obj.key
|
36
|
+
end
|
37
|
+
seen.sort.should == @keys
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'can inject' do
|
41
|
+
Enum.inject(0) do |count, obj|
|
42
|
+
count + obj.key.size
|
43
|
+
end.should == @keys.join('').size
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'GZip' do
|
4
|
+
class Zippable < Risky
|
5
|
+
include Risky::GZip
|
6
|
+
|
7
|
+
bucket :risky_gzip
|
8
|
+
value :value
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'can create and save a named object with a value' do
|
12
|
+
g = Zippable.new 'pizzazz', :value => 'oomph'
|
13
|
+
g.save.should == g
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can read objects' do
|
17
|
+
Zippable['zest'].should be_nil
|
18
|
+
|
19
|
+
g = Zippable.new 'zing', :value => 'vigor'
|
20
|
+
g.save.should_not be_false
|
21
|
+
|
22
|
+
g2 = Zippable['zing']
|
23
|
+
g2.should === g
|
24
|
+
g2.value.should == 'vigor'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'GZip with allow_mult' do
|
29
|
+
class MultiZippable < Risky
|
30
|
+
include Risky::GZip
|
31
|
+
|
32
|
+
bucket :risky_gzip_mult
|
33
|
+
allow_mult
|
34
|
+
|
35
|
+
value :value
|
36
|
+
|
37
|
+
def self.merge(versions)
|
38
|
+
g = super(versions)
|
39
|
+
g.value = versions.map(&:value).sort.join(', ')
|
40
|
+
g
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'merges' do
|
45
|
+
conflict(MultiZippable, 'value', ['spirit', 'sparkle']).value.should eq('sparkle, spirit')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'Legacy data not GZipped' do
|
50
|
+
class NoZip < Risky
|
51
|
+
bucket :risky_gzip_legacy
|
52
|
+
value :value
|
53
|
+
end
|
54
|
+
|
55
|
+
class Zip < Risky
|
56
|
+
include Risky::GZip
|
57
|
+
|
58
|
+
bucket :risky_gzip_legacy
|
59
|
+
value :value
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'loads non-zipped legacy data' do
|
63
|
+
g = NoZip.new 'gusto', :value => 'spritely'
|
64
|
+
g.save.should == g
|
65
|
+
|
66
|
+
g1 = Zip['gusto']
|
67
|
+
g1.key.should eq(g.key)
|
68
|
+
g1.value.should eq(g.value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|