acts_as_hashish 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ module Hashish
2
+ # Configuration
3
+ module Configuration
4
+ # Redis connection instance
5
+ attr_accessor :redis_connection
6
+
7
+ # Redis namespace for keys
8
+ attr_writer :redis_namespace
9
+
10
+ # Redis namespace for keys
11
+ attr_writer :redis_search_keys_ttl
12
+
13
+ def redis_namespace
14
+ @redis_namespace ||= 'hashish'
15
+ end
16
+
17
+ def redis_search_keys_ttl
18
+ @redis_search_keys_ttl ||= 5 # minutes
19
+ end
20
+
21
+ def configure
22
+ yield self
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,169 @@
1
+ module Hashish
2
+
3
+ def acts_as_hashish(options = {})
4
+ raise "Cannot act as hashish without a redis connection!" unless Hashish.redis_connection
5
+ options[:key_prefix] ||= Hashish.redis_namespace + ':' + self.to_s
6
+ raise "Please specify a primary index via the :key option!" unless options[:key]
7
+ options[:indexes] ||= {}
8
+ options[:sorters] ||= {}
9
+
10
+ @options = options
11
+ extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ private
17
+ def get_hashish_key(item)
18
+ key = @options[:key]
19
+ key_value = key.is_a?(Proc) ? key.call(item) : item[key]
20
+ end
21
+
22
+ public
23
+ # this needs some improvisation
24
+ def rebuild_indexes
25
+ get_list(:page_size => 0).each do |item|
26
+ hashish_insert(item)
27
+ end
28
+ end
29
+
30
+ def flush!
31
+ Hashish.redis_connection.keys("#{@options[:key_prefix]}*").each{|x| Hashish.redis_connection.del(x)}
32
+ end
33
+
34
+ def size(category = '')
35
+ Hashish.redis_connection.zcard("#{@options[:key_prefix]}:#{category}")
36
+ end
37
+
38
+ def hashish_delete(key = nil)
39
+ key ||= get_hashish_key(get_hashish_list.first)
40
+ prefix = @options[:key_prefix]
41
+ Hashish.redis_connection.smembers("#{prefix}:status:*").each do |v|
42
+ Hashish.redis_connection.zrem(v, "#{prefix}:#{key}")
43
+ end
44
+ Hashish.redis_connection.del("#{prefix}:#{key}")
45
+ end
46
+
47
+ def hashish_insert(o, category = '', t = Time.now.to_i)
48
+ data = o.to_json
49
+ prefix = @options[:key_prefix]
50
+ category += ':' unless category.empty?
51
+ key = get_hashish_key(o)
52
+
53
+ Hashish.redis_connection.zadd("#{prefix}:#{category}", t , "#{prefix}:#{key}")
54
+ Hashish.redis_connection.zremrangebyrank("#{prefix}:#{category}", 0, -(@options[:max_size] + 1)) if @options[:max_size]
55
+ Hashish.redis_connection.sadd("#{prefix}:status:*", "#{prefix}:#{category}")
56
+ @options[:indexes].each do |index_field, index|
57
+ index_value = index.is_a?(Proc) ? index.call(o) : o[index]
58
+ if index_value.is_a?(Array)
59
+ index_value.each do |i|
60
+ Hashish.redis_connection.zadd("#{prefix}:#{index_field}:#{i}", t, "#{prefix}:#{key}") if i.is_a?(String)
61
+ end
62
+ else
63
+ Hashish.redis_connection.zadd("#{prefix}:#{index_field}:#{index_value}", t, "#{prefix}:#{key}")
64
+ end
65
+ end
66
+ @options[:sorters].each do |sort_field, sort|
67
+ sort_value = sort.is_a?(Proc) ? sort.call(o) : o[sort]
68
+ Hashish.redis_connection.set("#{prefix}:#{key}:#{sort}", sort_value)
69
+ end
70
+ Hashish.redis_connection.set("#{prefix}:#{key}", data)
71
+ true
72
+ end
73
+
74
+ def hashish_list(*args)
75
+ category = nil
76
+
77
+ options = {}
78
+
79
+ # handle all kinds of argument structures that make sense
80
+ if args.length == 1
81
+ if args[0].is_a?(Hash)
82
+ options = args[0]
83
+ elsif args[0].is_a?(String)
84
+ category = args[0]
85
+ else
86
+ raise
87
+ end
88
+ elsif args.length > 1
89
+ category = args[0]
90
+ options = args[1]
91
+ end
92
+
93
+ category += ':' if category
94
+ page_no = options[:page_no] || 1
95
+ page_size = options[:page_size] || 10
96
+
97
+ max_time = options[:to] || '+inf'
98
+ min_time = options[:from] || '-inf'
99
+
100
+ filters = options[:filters] || {}
101
+ sort_by = options[:sort_by] || nil
102
+ sort_order = options[:sort_order] || nil
103
+
104
+ offset = (page_no - 1) * page_size
105
+ limit = offset + page_size
106
+
107
+ # get the next seq no for search operations for this search instance (perhaps this entire section shld be oops based stuff but what the heck :E mayb later)
108
+ seq = Hashish.redis_connection.incr("#{@options[:key_prefix]}:result:seq")
109
+
110
+ # search
111
+ inter = []
112
+ unless filters.empty?
113
+ # inter = filters.map{|k,v| "#{@options[:key_prefix]}:#{k}:#{v}"}
114
+
115
+ filters.each do |key, value|
116
+ if value.is_a?(Array)
117
+ union = []
118
+ union_key = "#{@options[:key_prefix]}:union:#{key}:#{seq}"
119
+ value.each do |v|
120
+ union << "#{@options[:key_prefix]}:#{key}:#{v}"
121
+ end
122
+ Hashish.redis_connection.zunionstore(union_key, union.uniq)
123
+ Hashish.redis_connection.expire(union_key, Hashish.redis_search_keys_ttl)
124
+ inter << union_key
125
+ else
126
+ inter << "#{@options[:key_prefix]}:#{key}:#{value}"
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+ full_list = "#{@options[:key_prefix]}:#{category}"
133
+
134
+ # is the user askin for a cropped set of data (min/max date/time of nQ)
135
+ if min_time == '-inf' and max_time == '+inf'
136
+ inter << full_list
137
+ else
138
+ # copy the full list to different temp set
139
+ all_items_key = "#{@options[:key_prefix]}:all_items:#{seq}"
140
+ Hashish.redis_connection.zunionstore(all_items_key, [full_list])
141
+ Hashish.redis_connection.expire(all_items_key, Hashish.redis_search_keys_ttl * 60)
142
+ # crop the set based on min/max (wish we had a 1 step zcroprangebyscore)
143
+ Hashish.redis_connection.zremrangebyscore(all_items_key, "-inf", "(#{min_time}") if min_time != '-inf'
144
+ Hashish.redis_connection.zremrangebyscore(all_items_key, "(#{max_time}", "+inf") if max_time != '+inf'
145
+ inter << all_items_key
146
+ end
147
+
148
+ result_key = "#{@options[:key_prefix]}:result:#{seq}"
149
+ Hashish.redis_connection.zinterstore(result_key, inter, :aggregate => 'max')
150
+ Hashish.redis_connection.expire(result_key, Hashish.redis_search_keys_ttl * 60)
151
+ result = nil
152
+ if sort_by
153
+ custom_sort = "#{@options[:key_prefix]}:custom_sort:#{seq}"
154
+ Hashish.redis_connection.sort(result_key, :by => "*:#{sort_by}",:get => '*', :store => custom_sort, :order => sort_order)
155
+ Hashish.redis_connection.expire(custom_sort, Hashish.redis_search_keys_ttl * 60)
156
+ result = Hashish.redis_connection.lrange(custom_sort, offset, limit -1)
157
+ else
158
+ res_keys = Hashish.redis_connection.zrevrange(result_key, offset, limit - 1)
159
+ if res_keys.empty?
160
+ result = []
161
+ else
162
+ result = Hashish.redis_connection.mget(*res_keys)
163
+ end
164
+ end
165
+ result.compact.map{|x| JSON.parse(x) rescue x}
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,3 @@
1
+ module Hashish
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,13 @@
1
+ require 'redis'
2
+ require 'json'
3
+ require 'acts_as_hashish'
4
+ require 'acts_as_hashish/version'
5
+ require 'acts_as_hashish/configuration'
6
+ require 'acts_as_hashish/hashish'
7
+
8
+ module Hashish
9
+ extend Configuration
10
+ end
11
+
12
+ Object.extend Hashish
13
+
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashish do
4
+
5
+ describe ".acts_as_hashish" do
6
+ it "should hashify the class" do
7
+ SampleClass.expects(:hashify)
8
+ SampleClass.acts_as_hashish(:key => 'id')
9
+ SampleClass.instance_variable_get(:@options)[:key_prefix].should eq(Hashish.redis_namespace + ":" + SampleClass.to_s)
10
+ puts SampleClass.methods.sort
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'acts_as_hashish'
3
+
4
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
5
+ require 'rspec/autorun'
6
+ require 'test/unit'
7
+ require 'mocha'
8
+
9
+ # Requires supporting ruby files with custom matchers and macros, etc,
10
+ # in spec/support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ RSpec.configure do |config|
14
+ config.mock_with :mocha
15
+ config.color_enabled = true
16
+
17
+ config.before(:all) do
18
+ Hashish.configure do |configuration|
19
+ configuration.redis_connection = Redis.new(:db => 15, :host => '172.16.54.24')
20
+ configuration.redis_namespace = 'hashish_test_namspace'
21
+ end
22
+ end
23
+
24
+ config.before(:each) do
25
+ Hashish.redis_connection.flushdb
26
+ class SampleClass
27
+ end
28
+ end
29
+
30
+ config.after(:each) do
31
+ Object.send(:remove_const, :SampleClass)
32
+ end
33
+
34
+ config.after(:all) do
35
+ Hashish.redis_connection.flushdb
36
+ Hashish.redis_connection.quit
37
+ end
38
+
39
+ # Run specs in random order to surface order dependencies. If you find an
40
+ # order dependency and want to debug it, you can fix the order by providing
41
+ # the seed, which is printed after each run.
42
+ # --seed 1234
43
+ config.order = "random"
44
+
45
+ # treats :focus to be true by default
46
+ # config.treat_symbols_as_metadata_keys_with_true_values = true
47
+
48
+ # runs only the specs who have focus tag
49
+ # config.filter_run_including :focus => true
50
+
51
+ # runs specs excluding specs with broken tag
52
+ # config.filter_run_excluding :broken => true
53
+
54
+ # runs all specs if filters are matched
55
+ # config.run_all_when_everything_filtered = true
56
+
57
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_hashish
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Schubert Cardozo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2013-01-25 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: redis
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: rake
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: rspec
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: mocha
61
+ prerelease: false
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ type: :development
69
+ version_requirements: *id005
70
+ description: A sortable and searchable list backed by Redis
71
+ email:
72
+ - cardozoschubert@gmail.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files: []
78
+
79
+ files:
80
+ - lib/acts_as_hashish/configuration.rb
81
+ - lib/acts_as_hashish/hashish.rb
82
+ - lib/acts_as_hashish/version.rb
83
+ - lib/acts_as_hashish.rb
84
+ - spec/hashish_spec.rb
85
+ - spec/spec_helper.rb
86
+ homepage: https://github.com/saturnine/acts_as_hashish
87
+ licenses: []
88
+
89
+ post_install_message:
90
+ rdoc_options: []
91
+
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 1.9.2
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: "0"
106
+ requirements: []
107
+
108
+ rubyforge_project: acts_as_hashish
109
+ rubygems_version: 1.8.19
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: A sortable and searchable list backed by Redis
113
+ test_files: []
114
+