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