risky 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ class Risky::PaginatedCollection < Array
2
+
3
+ attr_reader :keys, :continuation
4
+
5
+ def initialize(collection, paginated_keys)
6
+ @keys = paginated_keys
7
+ @continuation = paginated_keys.continuation
8
+
9
+ super(collection)
10
+ end
11
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Risky
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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
+