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.
@@ -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
+