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.
- data/lib/acts_as_hashish/configuration.rb +25 -0
- data/lib/acts_as_hashish/hashish.rb +169 -0
- data/lib/acts_as_hashish/version.rb +3 -0
- data/lib/acts_as_hashish.rb +13 -0
- data/spec/hashish_spec.rb +14 -0
- data/spec/spec_helper.rb +57 -0
- metadata +114 -0
@@ -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,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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|