lion-attr 0.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 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: