lion-attr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 34d61bee3070595310ca92f1f1ef9256aaebe603
4
+ data.tar.gz: ac04beac1d6b6bcacfabfe81583e5259f05fea54
5
+ SHA512:
6
+ metadata.gz: 31280e75824539ab18df77a855c69b20d4f54ec23c945beca643f1e264bb444b5e8b450d8c47852f94fb46db50b145829599fe24c98e30222e140b66ac962f6f
7
+ data.tar.gz: 40b783f03ea499fef042d7596ac9d9f1dad11fae3bc997907dd5ad83df47229b3317207d59d47f2f97883a155dea584d7c44a90a2d7092e4633a11ba4cb2b304
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Victor Tran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'LionAttr'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
data/lib/lion_attr.rb ADDED
@@ -0,0 +1,262 @@
1
+ require_relative 'lion_attr/internal_redis'
2
+ require_relative 'lion_attr/config'
3
+ require_relative 'lion_attr/bson'
4
+
5
+ # LionAttr, a gem to store Mongoid object in Redis for fast access. It also helps
6
+ # you to store object attributes to real time update via Redis increamental
7
+ # operations.
8
+ #
9
+ # In many web applications, updating objects to database (mongodb) too often
10
+ # would hurt overal application performance because databases usually need to
11
+ # do some extra tasks such as indexing, updating its cache ...
12
+ #
13
+ # One of the most popular example for this problem is tracking pageview of a web
14
+ # page. Imagine if you have a blog and you want to know how many time people visit
15
+ # a specific article, what would you do? An obvious way to do so is whenever a
16
+ # person make a request, you increase the record holding pageview counter by using
17
+ # `mongoid#inc` method. Doing this is not a good idea because it increases database
18
+ # calls making your application slower.
19
+ #
20
+ # A better solution for this problem is to keep the pageview counter in memory
21
+ # (which is fast) key-value storage such as Redis. Instead of increasing the
22
+ # counter by the database, we increase the counter inside Redis and save back to
23
+ # the database later (like after 10 mins, 30 mins or even a day).
24
+ #
25
+ # LionAttr provides APIs for you to do so with ease.
26
+ #
27
+ # That counter is usually an attribute of a Model object, the difference is that
28
+ # attribute will get the value from Redis instead of the database. We call it live
29
+ # attribute because it tends update its value very frequently.
30
+ #
31
+ # @example
32
+ #
33
+ # class Article
34
+ # include Mongoid::Document
35
+ # include LionAttr
36
+ # field :url, type: String
37
+ # field :view, type: Integer
38
+ #
39
+ # # field :view will be stored in Redis and saved back to Mongodb later
40
+ # live :view
41
+ # end
42
+ #
43
+ # @todo: Cache object
44
+ # @todo: Custom key
45
+ # @todo: Inc
46
+ # @todo: Callbacks
47
+ # @todo: Configure
48
+ #
49
+ # @since 0.1.0
50
+ #
51
+ # @see http://tranvictor.github.io/lion_attr
52
+
53
+ module LionAttr
54
+
55
+ # Including LionAttr will set an after save callback to update the object cache
56
+ # in Redis.
57
+ #
58
+ # @param base Mongoid::Document
59
+ def self.included(base)
60
+ base.extend(ClassMethods)
61
+ base.set_callback(:save, :after, :update_to_redis, prepend: true)
62
+ end
63
+
64
+ # Key to store in Redis, it is combined by object identity and field name.
65
+ #
66
+ # @param [String, Symbol] field
67
+ # @return [String]
68
+ # @example
69
+ # article.key(:view)
70
+ # #=> 54d5f10d5675730bd1050000_view
71
+ #
72
+ # article.url = "http://clicklion.com/article/1"
73
+ # article.class.live_key = :url
74
+ # article.key(:view)
75
+ # #=> http://clicklion.com/article/1_view
76
+ #
77
+ # @see ClassMethods#live_key Object Identity definition
78
+ def key(field)
79
+ self.class._key(self.send(self.class.live_key), field)
80
+ end
81
+
82
+ # Call this method to manually create a cache of the object in Redis.
83
+ # It will store the object as a json string in a Redis Hash with key is the
84
+ # object's id. Object from different classes will be stored in different hashes
85
+ # distinguished by class full name.
86
+ #
87
+ # @see InternalRedis
88
+ def update_to_redis
89
+ InternalRedis.new(self.class.name).set(id, as_document.to_json)
90
+ end
91
+
92
+ # Call this method will clear all of Redis keys related to the object.
93
+ def clean_cache_after_destroy
94
+ @live_keys ||= self.class.live_fields.map { |f| key(f) }
95
+ @internal_redis ||= InternalRedis.new(self.class.name)
96
+ @internal_redis.del @live_keys
97
+ @internal_redis.del id
98
+ end
99
+
100
+ module ClassMethods
101
+
102
+ # Fetch the object specified with an id from Redis. It will not touch the
103
+ # database (Mongdb). If that object is not available on Redis or invalid (due to
104
+ # model changes, it will make a query to the database (Mongodb).
105
+ #
106
+ # @param [String] id
107
+ # @return [Mongoid::Document]
108
+ def fetch(id)
109
+ @internal_redis ||= InternalRedis.new(name)
110
+ string_object = @internal_redis.get(id)
111
+ if string_object.nil?
112
+ object = _fetch_from_db(id)
113
+ else
114
+ object = new(JSON.load(string_object))
115
+ end
116
+ rescue Mongoid::Errors::UnknownAttribute
117
+ object = _fetch_from_db(id)
118
+ end
119
+
120
+ # Query the object by id, and create a cache version in Redis.
121
+ #
122
+ # @param [String] id
123
+ # @return [Mongoid::Document]
124
+ def _fetch_from_db(id)
125
+ object = find(id)
126
+ @internal_redis.set(id, object.as_document.to_json)
127
+ object
128
+ end
129
+
130
+ # Get all live fields of the class
131
+ #
132
+ # @example
133
+ # article.class.live_fields
134
+ # #=> [:view]
135
+ def live_fields
136
+ @live_fields
137
+ end
138
+
139
+ def live(*fields)
140
+ fields.each do |field|
141
+ generate_fetch_cache_method(field)
142
+ # generate_set_cache_method(field)
143
+ end
144
+ generate_update_db_method(fields)
145
+ generate_incr_method
146
+ (@live_fields ||= []).push(*fields)
147
+ set_callback(:destroy, :after, :clean_cache_after_destroy)
148
+ end
149
+
150
+ # Specify the field which is used to get storage key for the object.
151
+ #
152
+ # @param [String, Symbole] field
153
+ # @example
154
+ # article.class.live_key = :url
155
+ # # Whenever LionAttr need to use object key, it gets from :url
156
+ def live_key=(field)
157
+ @key = field
158
+ end
159
+
160
+ # Get object key to interact with Redis. If you don't specify the live_key
161
+ # :id will be used by default.
162
+ def live_key
163
+ @key || :id
164
+ end
165
+
166
+ def generate_update_db_method(_fields)
167
+ define_method('update_db') do
168
+ @live_keys ||= self.class.live_fields.map { |f| key(f) }
169
+ @internal_redis ||= InternalRedis.new(self.class.name)
170
+ redis_values = @internal_redis.mget(@live_keys)
171
+ self.class.live_fields.each_with_index do |f, i|
172
+ if read_attribute(f) != redis_values[i]
173
+ write_attribute(f, redis_values[i])
174
+ end
175
+ end
176
+ # TODO: This could be improved by using batch save instead of saving
177
+ # individual document
178
+ save
179
+ end
180
+ end
181
+
182
+ def _key(id, field)
183
+ "#{id}_#{field}"
184
+ end
185
+
186
+ # Increase live attributes value in Redis. If the live attributes are not
187
+ # Integer nor Float, a String message will be returned. Otherwise, increased
188
+ # value will be returned.
189
+ #
190
+ # @param [String] id
191
+ # @param [String, Symbol] field
192
+ # @param [Integer] increment
193
+ # @param [InternalRedis] internal_redis
194
+ #
195
+ # @return [Integer, Float, String]
196
+ def incr(id, field, increment = 1, internal_redis = nil)
197
+ unless self.live_fields.include?(field)
198
+ fail "#{field} is not a live attributes"
199
+ end
200
+ internal_redis ||= InternalRedis.new(name)
201
+ _incr(_key(id, field), fields[field.to_s].type,
202
+ @internal_redis, increment) do
203
+ find(id).read_attribute(field)
204
+ end
205
+ rescue => e
206
+ e.message
207
+ end
208
+
209
+ def _incr(key, type, internal_redis = nil, increment = 1, &block)
210
+ internal_redis ||= InternalRedis.new(name)
211
+ if block && !internal_redis.exists(key)
212
+ internal_redis.setnx key, block.call
213
+ end
214
+ if type == Integer
215
+ internal_redis.incrby(key, increment)
216
+ elsif type == Float
217
+ internal_redis.incrbyfloat(key, increment)
218
+ else
219
+ 'ERR hash value is not a number'
220
+ end
221
+ end
222
+
223
+ def generate_incr_method
224
+ define_method('incr') do |field, increment = 1|
225
+ begin
226
+ unless self.class.live_fields.include?(field)
227
+ fail "#{field} is not a live attributes"
228
+ end
229
+ @internal_redis ||= InternalRedis.new(self.class.name)
230
+ self.class._incr(key(field),
231
+ fields[field.to_s].type,
232
+ @internal_redis,
233
+ increment) { read_attribute(field) }
234
+ rescue => e
235
+ e.message
236
+ end
237
+ end
238
+ end
239
+
240
+ def generate_fetch_cache_method(name)
241
+ re_define_method("#{name}") do
242
+ @internal_redis ||= InternalRedis.new(self.class.name)
243
+ raw = @internal_redis.get(key(name))
244
+ field = fields[name.to_s]
245
+ if raw.nil?
246
+ raw = read_attribute(name)
247
+ if lazy_settable?(field, raw)
248
+ value = write_attribute(name, field.eval_default(self))
249
+ else
250
+ value = field.demongoize(raw)
251
+ attribute_will_change!(name) if value.resizable?
252
+ end
253
+
254
+ @internal_redis.set(key(name), raw)
255
+ else
256
+ value = field.demongoize(raw)
257
+ end
258
+ value
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,11 @@
1
+ module BSON
2
+ class ObjectId
3
+ def to_json(*_args)
4
+ to_s.to_json
5
+ end
6
+
7
+ def as_json(*_args)
8
+ to_s.as_json
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ module LionAttr
4
+
5
+ # This module defines all the configuration options for LionAttr
6
+ module Config
7
+ def redis_config=(options)
8
+ @redis_config = options
9
+ RedisPool.instance.update_pool
10
+ end
11
+
12
+ def redis_config
13
+ @redis_config ||= {}
14
+ end
15
+
16
+ extend self
17
+ end
18
+
19
+ module ModuleInterface
20
+ # Sets the LionAttr configuration options. Best used by passing a block.
21
+ # You should configure before actually using LionAttr.
22
+ #
23
+ # @example Set up configuration options and tell LionAttr to store everything in
24
+ # redis datababse 13
25
+ #
26
+ # LionAttr.configure do |config|
27
+ # config.redis_config = { db: 13 }
28
+ # end
29
+ #
30
+ # @return [ Config ] The configuration object.
31
+ #
32
+ # @since 0.1.0
33
+ def configure
34
+ block_given? ? yield(::LionAttr::Config) : ::LionAttr::Config
35
+ end
36
+ end
37
+ extend ModuleInterface
38
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'redis_pool'
2
+
3
+ module LionAttr
4
+ class InternalRedis
5
+ def initialize(hash_name)
6
+ @hash_name = hash_name
7
+ end
8
+
9
+ def get(key)
10
+ redis.with { |c| c.hget @hash_name, key }
11
+ end
12
+
13
+ def set(key, value)
14
+ redis.with { |c| c.hset @hash_name, key, value }
15
+ end
16
+
17
+ def exists(key)
18
+ redis.with { |c| c.hexists @hash_name, key }
19
+ end
20
+
21
+ def del(key)
22
+ redis.with { |c| c.hdel @hash_name, key }
23
+ end
24
+
25
+ def setnx(key, value)
26
+ redis.with { |c| c.hsetnx @hash_name, key, value }
27
+ end
28
+
29
+
30
+ def incrby(key, increment)
31
+ redis.with { |c| c.hincrby @hash_name, key, increment }
32
+ end
33
+
34
+ def incrbyfloat(key, increment)
35
+ redis.with { |c| c.hincrbyfloat @hash_name, key, increment }
36
+ end
37
+
38
+ def mget(keys)
39
+ redis.with { |c| c.hmget @hash_name, keys }
40
+ end
41
+
42
+ def redis
43
+ RedisPool.instance
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ require 'singleton'
2
+ require 'redis'
3
+ require 'connection_pool'
4
+
5
+ module LionAttr
6
+ class RedisPool
7
+ include Singleton
8
+
9
+ def initialize
10
+ update_pool
11
+ end
12
+
13
+ def update_pool
14
+ @pool = ConnectionPool.new(size: 100, timeout: 2) do
15
+ puts ::LionAttr.configure.redis_config
16
+ Redis.new ::LionAttr.configure.redis_config
17
+ end
18
+ end
19
+
20
+ def with(&block)
21
+ @pool.with(&block)
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module LionAttr
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :lion_attr do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe LionAttr do
4
+ it 'can be configured via #configuree by passing a block' do
5
+ LionAttr.configure do |config|
6
+ config.redis_config = { :a => 1 }
7
+ end
8
+
9
+ expect(LionAttr::Config.redis_config[:a]).to eq 1
10
+ end
11
+
12
+ describe "#configure" do
13
+ it 'returns config object' do
14
+ expect(LionAttr.configure).to be LionAttr::Config
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,290 @@
1
+ require 'spec_helper'
2
+
3
+ class TestRedis
4
+ include Mongoid::Document
5
+ field :foo, type: Integer
6
+ field :bar, type: Integer
7
+ field :mew, type: Float
8
+ field :baz, type: Float
9
+ field :qux, type: String
10
+ include LionAttr
11
+ live :foo, :bar, :mew, :qux
12
+ end
13
+
14
+ describe LionAttr do
15
+ before(:each) do
16
+ @test = TestRedis.new
17
+ @redis = LionAttr::InternalRedis.new(@test.class.name)
18
+ end
19
+
20
+ describe "class#incr" do
21
+ before(:each) do
22
+ @test.save
23
+ end
24
+
25
+ it 'defines an incr method on the class' do
26
+ expect(@test.class).to respond_to(:incr)
27
+ end
28
+
29
+ it 'should change attribute value in redis' do
30
+ @test.foo = 0
31
+ @redis.set(@test.key(:foo), 0)
32
+ expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
33
+ TestRedis.incr(@test.id, :foo, 10)
34
+ expect(@redis.get(@test.key(:foo))).not_to eq((@test.foo + 10).to_s)
35
+ end
36
+
37
+ it 'should work fine if the attribute is not initialized to redis yet' do
38
+ @test.foo = 50
39
+ @test.save
40
+ TestRedis.incr @test.id, :foo, 1
41
+ expect(@test.foo).to eq 51
42
+ end
43
+
44
+ it 'should not be affected by fetch' do
45
+ @test.foo = 0
46
+ @test.save
47
+ @test.incr :foo, 10
48
+ @new_test = TestRedis.fetch(@test.id)
49
+ TestRedis.incr @test.id, :foo, 10
50
+ expect(@new_test.foo).to eq 20
51
+ end
52
+
53
+ it 'should return attribute value in redis' do
54
+ expect(TestRedis.incr(@test.id, :foo,
55
+ 10).to_s).to eq(@redis.get(@test.key(:foo)))
56
+ end
57
+
58
+ it 'should rescue error by return error message' do
59
+ #increase integer field by float increment
60
+ err_msg = "ERR value is not an integer or out of range"
61
+ expect(TestRedis.incr(@test.id, :foo, 10.0)).to eq(err_msg)
62
+
63
+ #increase float field by string
64
+ err_msg = 'ERR value is not a valid float'
65
+ expect(TestRedis.incr(@test.id, :mew, 'a')).to eq(err_msg)
66
+
67
+ #increase string field
68
+ err_msg = 'ERR hash value is not a number'
69
+ expect(TestRedis.incr(@test.id, :qux, 10)).to eq(err_msg)
70
+ end
71
+
72
+ it 'should set default value for increment is 1' do
73
+ @redis.set(@test.key(:foo), 0)
74
+ TestRedis.incr @test.id, :foo
75
+ expect(@redis.get(@test.key(:foo))).to eq(1.to_s)
76
+ end
77
+
78
+ it 'should raise exception when incr not live attribute' do
79
+ err_msg = 'baz is not a live attributes'
80
+ expect(@test.incr(:baz)).to eq(err_msg)
81
+ end
82
+ end
83
+
84
+ describe 'more methods' do
85
+ it 'should generate method to fetch attributes value from cache' do
86
+ expect(@test).to respond_to(:foo)
87
+ expect(@test).to respond_to(:foo=)
88
+ expect(@test).to respond_to(:bar)
89
+ expect(@test).to respond_to(:bar=)
90
+ expect(@test).to respond_to(:update_db)
91
+ expect(@test).to respond_to(:incr)
92
+ expect(@test.class).to respond_to(:fetch)
93
+ expect(@test.class).to respond_to(:live_fields)
94
+ end
95
+ end
96
+
97
+ describe 'callback' do
98
+ it 'should set callback function' do
99
+ expect(@test).to receive(:clean_cache_after_destroy)
100
+ @test.destroy
101
+ end
102
+
103
+ it 'should remove live attribute in redis if object was destroy' do
104
+ @redis.set(@test.key(:foo), 10)
105
+ @redis.set(@test.key(:bar), 10)
106
+ @test.destroy
107
+ expect(@redis.get(@test.key(:foo))).to eq(nil)
108
+ expect(@redis.get(@test.key(:bar))).to eq(nil)
109
+ end
110
+
111
+ it 'should remove object in redis if object was destroyed' do
112
+ id = @test.id
113
+ @test.save
114
+ @test.class.fetch(id)
115
+ expect(@redis.get(id)).to eq(@test.as_document.to_json)
116
+ @test.destroy
117
+ expect(@redis.get(id)).to eq(nil)
118
+ end
119
+ end
120
+
121
+ describe '#fetch' do
122
+ it 'should return object if it exist in redis' do
123
+ @redis.set(@test.id, @test.as_document.to_json)
124
+ expect(@test.class.fetch(@test.id)).to eq(@test)
125
+ end
126
+
127
+ it 'should set object in to redis in case not exist in redis yet' do
128
+ id = @test.id
129
+ expect(@redis.get(id)).to eq(nil)
130
+ @test.save
131
+ @test.class.fetch(id)
132
+ expect(@redis.get(id)).to eq(@test.as_document.to_json)
133
+ end
134
+
135
+ it 'should return updated object if original was updated' do
136
+ @test.save
137
+ @old_test = TestRedis.fetch(@test.id)
138
+ @test.update(:baz => 100)
139
+ @updated_test = TestRedis.fetch(@test.id)
140
+ expect(@updated_test.baz).to eq 100
141
+ end
142
+
143
+ it 'should be ok if the model is changed while cached object
144
+ is still available, it will refetch from db if the cached version is invalid' do
145
+ @test.save
146
+ document = @test.as_document
147
+ document['will_be_removed'] = 10
148
+ json_data = document.to_json
149
+ @redis.set(@test.id, json_data)
150
+ expect { @new_test = TestRedis.fetch(@test.id) }.not_to raise_error
151
+ end
152
+ end
153
+
154
+ describe '#key' do
155
+ it 'should contain object id and field name' do
156
+ expect(@test.key(:foo)).to eq("#{@test.id}_foo")
157
+ puts @test.key(:foo)
158
+ end
159
+ end
160
+
161
+ describe '#update_db' do
162
+ it 'should update mongodb record if attribute value
163
+ is difference from redis' do
164
+ @test.incr(:foo, 10)
165
+ expect(@redis.get(@test.key(:foo))).not_to eq(@test.read_attribute(:foo).to_s)
166
+ @test.update_db
167
+ expect(@redis.get(@test.key(:foo))).to eq(@test.read_attribute(:foo).to_s)
168
+ end
169
+ end
170
+
171
+ describe 'live attributes' do
172
+ it 'can be called multiple times' do
173
+ class TestMultipleLive
174
+ include Mongoid::Document
175
+ include LionAttr
176
+ field :foo, type: Integer
177
+ live :foo
178
+ field :bar, type: Integer
179
+ live :bar
180
+ include LionAttr
181
+ end
182
+ expect(TestMultipleLive.live_fields).to include(:foo, :bar)
183
+ @test = TestMultipleLive.new
184
+ expect(@test).to receive(:update_to_redis).exactly(1).times
185
+ @test.save
186
+ end
187
+
188
+ context 'custom key' do
189
+ it 'has default key of :id but can be assigned to other field' do
190
+ class TestCustomKey
191
+ include Mongoid::Document
192
+ include LionAttr
193
+ field :foo, type: Integer
194
+ live :bar
195
+ end
196
+ expect(TestCustomKey.live_key).to eq :id
197
+ TestCustomKey.class_eval do
198
+ self.live_key = :bar
199
+ end
200
+ expect(TestCustomKey.live_key).to eq :bar
201
+ end
202
+
203
+ it 'should change the key to save to redis' do
204
+ class TestCustomKey1
205
+ include Mongoid::Document
206
+ include LionAttr
207
+ field :foo, type: Integer
208
+ field :bar
209
+ live :bar
210
+ self.live_key = :foo
211
+ end
212
+ @instance = TestCustomKey1.new :foo => 100
213
+ expect(@instance.key(:bar)).to eq "100_bar"
214
+ end
215
+ end
216
+
217
+ describe '#incr' do
218
+ it 'should change attribute value in redis' do
219
+ @test.foo = 0
220
+ @redis.set(@test.key(:foo), 0)
221
+ expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
222
+ @test.incr(:foo, 10)
223
+ expect(@redis.get(@test.key(:foo))).not_to eq((@test.foo + 10).to_s)
224
+ end
225
+
226
+ it 'should work fine if the attribute is not initialized to redis yet' do
227
+ @test.foo = 50
228
+ @test.save
229
+ @test.incr :foo, 1
230
+ expect(@test.foo).to eq 51
231
+ end
232
+
233
+ it 'should not be affected by fetch' do
234
+ @test.foo = 0
235
+ @test.save
236
+ @test.incr :foo, 10
237
+ @new_test = TestRedis.fetch(@test.id)
238
+ @new_test.incr :foo, 10
239
+ expect(@new_test.foo).to eq 20
240
+ end
241
+
242
+ it 'should return attribute value in redis' do
243
+ expect(@test.incr(:foo, 10).to_s).to eq(@redis.get(@test.key(:foo)))
244
+ end
245
+
246
+ it 'should rescue error by return error message' do
247
+ #increase integer field by float increment
248
+ err_msg = "ERR value is not an integer or out of range"
249
+ expect(@test.incr(:foo, 10.0)).to eq(err_msg)
250
+
251
+ #increase float field by string
252
+ err_msg = 'ERR value is not a valid float'
253
+ expect(@test.incr(:mew, 'a')).to eq(err_msg)
254
+
255
+ #increase string field
256
+ err_msg = 'ERR hash value is not a number'
257
+ expect(@test.incr(:qux, 10)).to eq(err_msg)
258
+ end
259
+
260
+ it 'should set default value for increment is 1' do
261
+ @redis.set(@test.key(:foo), 0)
262
+ @test.incr(:foo)
263
+ expect(@redis.get(@test.key(:foo))).to eq(1.to_s)
264
+ end
265
+
266
+ it 'should raise exception when incr not live attribute' do
267
+ err_msg = 'baz is not a live attributes'
268
+ expect(@test.incr(:baz)).to eq(err_msg)
269
+ end
270
+ end
271
+
272
+ describe '#getter' do
273
+ it 'should read attribute value from db
274
+ in case the value is not exist in redis' do
275
+ # redis not contain attribute value
276
+ expect(@redis.get(@test.key(:foo))).to eq(nil)
277
+ # fetch cache still return attribute value
278
+ expect(@test.foo).to eq(@test.read_attribute(:foo))
279
+ # redis should contain attribute value now
280
+ expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
281
+ end
282
+
283
+ it 'should return attribute value from redis if it exist in redis' do
284
+ @test.foo = 10
285
+ @redis.set(@test.key(:foo), 20)
286
+ expect(@test.foo).to eq(20)
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe LionAttr do
4
+
5
+ describe "configuration" do
6
+ it "can be configured to specific redis config" do
7
+ LionAttr.configure do |config|
8
+ puts "setting configuration"
9
+ config.redis_config = { :db => 13 }
10
+ end
11
+
12
+ class TestClass
13
+ include Mongoid::Document
14
+ include LionAttr
15
+ field :foo, type: Integer
16
+
17
+ live :foo
18
+ end
19
+
20
+ @test = TestClass.new
21
+ @test.save
22
+ redis = Redis.new :db => 13
23
+ expect(redis.exists "TestClass").to be true
24
+ end
25
+ end
26
+ end
data/spec/mongoid.yml ADDED
@@ -0,0 +1,6 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: lion_attr_test
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ require 'bundler/setup'
3
+ Bundler.setup
4
+
5
+ require 'mongoid'
6
+
7
+ Mongoid.load!("#{File.dirname(__FILE__)}/mongoid.yml", :test)
8
+
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
10
+
11
+ require 'lion_attr'
12
+
13
+ RSpec.configure do |config|
14
+ config.order = :random
15
+
16
+ config.color = true
17
+
18
+ config.tty = true
19
+
20
+ config.expect_with :rspec do |expectations|
21
+ expectations.syntax = :expect
22
+ end
23
+
24
+ config.mock_with :rspec do |mocks|
25
+ mocks.syntax = :expect
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lion-attr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Victor Tran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: connection_pool
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ description: LionAttr uses Redis to store Mongoid object in-mem giving it fast accessibility.
70
+ It also gives convenience to manipulate numeric attributes which will be called
71
+ live attrbutes. With live attributes, you can increase, decrease database for high
72
+ performance.
73
+ email:
74
+ - vu.tran54@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - MIT-LICENSE
80
+ - Rakefile
81
+ - lib/lion_attr.rb
82
+ - lib/lion_attr/bson.rb
83
+ - lib/lion_attr/config.rb
84
+ - lib/lion_attr/internal_redis.rb
85
+ - lib/lion_attr/redis_pool.rb
86
+ - lib/lion_attr/version.rb
87
+ - lib/tasks/lion_attr_tasks.rake
88
+ - spec/config_spec.rb
89
+ - spec/lion_attr_spec.rb
90
+ - spec/lion_attr_with_custom_configuration_spec.rb
91
+ - spec/mongoid.yml
92
+ - spec/spec_helper.rb
93
+ homepage: http://github.com/tranvictor
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.4.3
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: LionAttr gives convenience of caching Mongoid objects and increase counter
117
+ without touching database.
118
+ test_files:
119
+ - spec/config_spec.rb
120
+ - spec/lion_attr_spec.rb
121
+ - spec/lion_attr_with_custom_configuration_spec.rb
122
+ - spec/mongoid.yml
123
+ - spec/spec_helper.rb
124
+ has_rdoc: