redis-persistence 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/README.markdown +51 -0
- data/Rakefile +12 -0
- data/examples/article.rb +82 -0
- data/examples/benchmark.rb +62 -0
- data/examples/rails-template.rb +124 -0
- data/lib/redis/persistence.rb +188 -0
- data/lib/redis/persistence/version.rb +5 -0
- data/redis-persistence.gemspec +32 -0
- data/test/_helper.rb +27 -0
- data/test/active_model_lint_test.rb +18 -0
- data/test/models.rb +106 -0
- data/test/redis_persistence_test.rb +293 -0
- metadata +164 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
Redis Persistence
|
2
|
+
=================
|
3
|
+
|
4
|
+
`Redis::Persistence` is a simple persistence layer for Ruby objects, fully compatible with ActiveModel,
|
5
|
+
and thus easily used both standalone or in a Rails project.
|
6
|
+
|
7
|
+
## Usage ##
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'redis/persistence'
|
11
|
+
|
12
|
+
Redis::Persistence.config.redis = Redis.new
|
13
|
+
# => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.1)>
|
14
|
+
|
15
|
+
class Article
|
16
|
+
include Redis::Persistence
|
17
|
+
|
18
|
+
property :id
|
19
|
+
property :title
|
20
|
+
property :body
|
21
|
+
property :author, :default => '(Unknown)'
|
22
|
+
end
|
23
|
+
|
24
|
+
article = Article.new :id => 1, :title => 'Lorem Ipsum'
|
25
|
+
# => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
|
26
|
+
|
27
|
+
article.save
|
28
|
+
# => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
|
29
|
+
|
30
|
+
article = Article.find(1)
|
31
|
+
# => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
|
32
|
+
|
33
|
+
article.title
|
34
|
+
# => "Lorem Ipsum"
|
35
|
+
|
36
|
+
article.author
|
37
|
+
# => "(Unknown)"
|
38
|
+
```
|
39
|
+
|
40
|
+
It comes with the standard feature set of ActiveModel classes: validations, callbacks, serialization,
|
41
|
+
Rails DOM helpers compatibility, etc.
|
42
|
+
|
43
|
+
|
44
|
+
## Installation ##
|
45
|
+
|
46
|
+
git clone git://github.com/Ataxo/redis-persistence.git
|
47
|
+
rake install
|
48
|
+
|
49
|
+
-----
|
50
|
+
|
51
|
+
(c) 2011, Ataxo Interactive, s.r.o., released under the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
Rake::TestTask.new(:test) do |test|
|
8
|
+
test.libs << 'lib' << 'test'
|
9
|
+
test.test_files = FileList['test/*_test.rb']
|
10
|
+
test.verbose = true
|
11
|
+
# test.warning = true
|
12
|
+
end
|
data/examples/article.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'redis/persistence'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
|
6
|
+
Redis::Persistence.config.redis = Redis.new :db => 14
|
7
|
+
Redis::Persistence.config.redis.flushdb
|
8
|
+
# => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.1)>
|
9
|
+
|
10
|
+
class Comment
|
11
|
+
def initialize(params); @attributes = HashWithIndifferentAccess.new(params); end
|
12
|
+
def method_missing(method_name, *arguments); @attributes[method_name]; end
|
13
|
+
def as_json(*); @attributes; end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Article
|
17
|
+
include Redis::Persistence
|
18
|
+
|
19
|
+
property :title
|
20
|
+
property :body
|
21
|
+
property :author, :default => '(Unknown)'
|
22
|
+
property :created
|
23
|
+
|
24
|
+
property :comments, :default => [], :class => [Comment], :family => 'comments'
|
25
|
+
end
|
26
|
+
|
27
|
+
article = Article.new :title => 'Do Not Blink',
|
28
|
+
:author => 'Malcom Gladwell',
|
29
|
+
:body => 'Imagine that I asked you ...',
|
30
|
+
:created => Time.now.utc
|
31
|
+
# => #<Article: {"id"=>1, "title"=>"Do Not Blink", ...>
|
32
|
+
|
33
|
+
p article.save
|
34
|
+
# => #<Article: {"id"=>1, "title"=>"Do Not Blink", ...>
|
35
|
+
|
36
|
+
p article = Article.find(1)
|
37
|
+
# => #<Article: {"id"=>1, "title"=>"Do Not Blink", ...>
|
38
|
+
|
39
|
+
p article.title
|
40
|
+
# => "Do Not Blink"
|
41
|
+
|
42
|
+
p article.created.year
|
43
|
+
# => 2011
|
44
|
+
|
45
|
+
article = Article.new :title => 'In the Beginning Was the Command Line'
|
46
|
+
p article.save
|
47
|
+
# => #<Article: {"id"=>2, "title"=>"In the Beginning Was the Command Line", ... "author"=>"(Unknown)"}>
|
48
|
+
|
49
|
+
p article = Article.find(2)
|
50
|
+
# => #<Article: {"id"=>2, "title"=>"In the Beginning Was the Command Line", ... "author"=>"(Unknown)"}>
|
51
|
+
|
52
|
+
p article.author
|
53
|
+
# => "(Unknown)"
|
54
|
+
|
55
|
+
article = Article.create :title => 'OMG BLOG!'
|
56
|
+
|
57
|
+
p article.comments
|
58
|
+
# => []
|
59
|
+
|
60
|
+
article.comments << {:nick => '4chan', :body => 'WHY U NO QUIT?'}
|
61
|
+
|
62
|
+
article.comments << Comment.new(:nick => 'h4x0r', :body => 'WHY U NO USE BBS?')
|
63
|
+
|
64
|
+
p article.comments.size
|
65
|
+
# => 2
|
66
|
+
|
67
|
+
p article.save
|
68
|
+
# => <Article: {"id"=>3, ... "comments"=>[{:nick=>"4chan", :body=>"WHY U NO QUIT?"}]}>
|
69
|
+
|
70
|
+
article = Article.find(3)
|
71
|
+
p article.comments
|
72
|
+
# => []
|
73
|
+
|
74
|
+
article = Article.find(3, :families => 'comments')
|
75
|
+
p article.comments
|
76
|
+
# => [#<Comment:0x007f893180dd08 @attributes={"nick"=>"4chan", "body"=>"WHY U NO QUIT?"}>]
|
77
|
+
|
78
|
+
p article.comments.first.nick
|
79
|
+
# => "4chan"
|
80
|
+
|
81
|
+
p article.comments.last.nick
|
82
|
+
# => "h4x0r"
|
@@ -0,0 +1,62 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'redis/persistence'
|
5
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
6
|
+
|
7
|
+
content = DATA.read
|
8
|
+
COUNT = ENV['COUNT'] || 100_000
|
9
|
+
|
10
|
+
Redis::Persistence.config.redis = Redis.new :db => 14
|
11
|
+
Redis::Persistence.config.redis.flushdb
|
12
|
+
|
13
|
+
class Article
|
14
|
+
include Redis::Persistence
|
15
|
+
|
16
|
+
property :title
|
17
|
+
property :content, :family => 'extra'
|
18
|
+
property :created, :family => 'extra'
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "Saving #{COUNT} documents into Redis..."
|
22
|
+
|
23
|
+
elapsed = Benchmark.realtime do
|
24
|
+
(1..COUNT).map do |i|
|
25
|
+
Article.create title: "Document #{i}", content: content, created: Time.now.utc
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
30
|
+
'-'*80
|
31
|
+
|
32
|
+
puts "Finding #{COUNT} documents one by one..."
|
33
|
+
|
34
|
+
elapsed = Benchmark.realtime do
|
35
|
+
(1..COUNT).map do |i|
|
36
|
+
Article.find(i, :family => 'extra')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
41
|
+
'-'*80
|
42
|
+
|
43
|
+
puts "Finding first 1000 documents with only 'data' family..."
|
44
|
+
|
45
|
+
elapsed = Benchmark.realtime do
|
46
|
+
Article.find (1..1000).to_a
|
47
|
+
end
|
48
|
+
|
49
|
+
puts "Duration: #{elapsed*1000} milliseconds, rate: #{1000.0/elapsed} docs/sec, #{(elapsed/1000.0)*1000.0} msec/doc",
|
50
|
+
'-'*80
|
51
|
+
|
52
|
+
puts "Updating all documents in batches of 1000..."
|
53
|
+
|
54
|
+
elapsed = Benchmark.realtime do
|
55
|
+
Article.find_each { |document| document.title += ' (touched)' and document.save }
|
56
|
+
end
|
57
|
+
|
58
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
59
|
+
'-'*80
|
60
|
+
|
61
|
+
__END__
|
62
|
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# =======================================================================
|
2
|
+
# Template for generating basic Rails application with Redis::Persistence
|
3
|
+
# =======================================================================
|
4
|
+
#
|
5
|
+
# Requirements
|
6
|
+
# ------------
|
7
|
+
#
|
8
|
+
# * Git
|
9
|
+
# * Ruby >= 1.9.3
|
10
|
+
# * Rubygems
|
11
|
+
# * Rails >= 3.1.0
|
12
|
+
# * Redis >= 2.4.1
|
13
|
+
#
|
14
|
+
#
|
15
|
+
# Usage
|
16
|
+
# -----
|
17
|
+
#
|
18
|
+
# $ rails new articles -m https://raw.github.com/Ataxo/redis-persistence/master/examples/rails-template.rb
|
19
|
+
#
|
20
|
+
# ===================================================================================================================
|
21
|
+
|
22
|
+
require 'rubygems'
|
23
|
+
|
24
|
+
run "rm public/index.html"
|
25
|
+
run "rm public/images/rails.png"
|
26
|
+
run "touch tmp/.gitignore log/.gitignore vendor/.gitignore"
|
27
|
+
|
28
|
+
run "rm -f .gitignore"
|
29
|
+
file ".gitignore", <<-END.gsub(/ /, '')
|
30
|
+
.DS_Store
|
31
|
+
log/*.log
|
32
|
+
tmp/**/*
|
33
|
+
config/database.yml
|
34
|
+
db/*.sqlite3
|
35
|
+
END
|
36
|
+
|
37
|
+
git :init
|
38
|
+
git :add => '.'
|
39
|
+
git :commit => "-m 'Initial commit: Clean application'"
|
40
|
+
|
41
|
+
puts
|
42
|
+
say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
|
43
|
+
puts '-'*80, ''; sleep 1
|
44
|
+
|
45
|
+
gem 'redis-persistence', :git => 'git://github.com/Ataxo/redis-persistence.git'
|
46
|
+
|
47
|
+
git :add => '.'
|
48
|
+
git :commit => "-m 'Added gems'"
|
49
|
+
|
50
|
+
puts
|
51
|
+
say_status "Rubygems", "Installing Rubygems...", :yellow
|
52
|
+
puts '-'*80, ''
|
53
|
+
|
54
|
+
run "bundle install"
|
55
|
+
|
56
|
+
puts
|
57
|
+
say_status "Model", "Adding the Article resource...", :yellow
|
58
|
+
puts '-'*80, ''; sleep 1
|
59
|
+
|
60
|
+
generate :scaffold, "Article title:string content:text published:date"
|
61
|
+
route "root :to => 'articles#index'"
|
62
|
+
|
63
|
+
git :add => '.'
|
64
|
+
git :commit => "-m 'Added the Article resource'"
|
65
|
+
|
66
|
+
puts
|
67
|
+
say_status "Model", "Adding Redis::Persistence into the Article model...", :yellow
|
68
|
+
puts '-'*80, ''; sleep 1
|
69
|
+
|
70
|
+
run "rm -f app/models/article.rb"
|
71
|
+
file 'app/models/article.rb', <<-CODE
|
72
|
+
class Article
|
73
|
+
include Redis::Persistence
|
74
|
+
|
75
|
+
property :title
|
76
|
+
property :content
|
77
|
+
property :published
|
78
|
+
end
|
79
|
+
CODE
|
80
|
+
|
81
|
+
initializer 'redis-persistence.rb', <<-CODE
|
82
|
+
Redis::Persistence.config.redis = Redis.new(:db => 14)
|
83
|
+
CODE
|
84
|
+
|
85
|
+
git :commit => "-a -m 'Added Redis::Persistence into the Article model, added initializer (Redis DB=14)'"
|
86
|
+
|
87
|
+
puts
|
88
|
+
say_status "Database", "Seeding the database with data...", :yellow
|
89
|
+
puts '-'*80, ''; sleep 0.25
|
90
|
+
|
91
|
+
run "rm -rf db/migrate"
|
92
|
+
run "redis-cli -n 14 flushdb"
|
93
|
+
run "rm -f db/seeds.rb"
|
94
|
+
file 'db/seeds.rb', <<-CODE
|
95
|
+
contents = [
|
96
|
+
'Lorem ipsum dolor sit amet.',
|
97
|
+
'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
|
98
|
+
'Labore et dolore magna aliqua.',
|
99
|
+
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
|
100
|
+
'Excepteur sint occaecat cupidatat non proident.'
|
101
|
+
]
|
102
|
+
|
103
|
+
puts "Creating articles..."
|
104
|
+
%w[ One Two Three Four Five ].each_with_index do |title, i|
|
105
|
+
Article.create title: title, content: contents[i], published: i.days.ago.utc
|
106
|
+
end
|
107
|
+
CODE
|
108
|
+
|
109
|
+
rake "db:seed"
|
110
|
+
|
111
|
+
git :add => "db/seeds.rb"
|
112
|
+
git :commit => "-m 'Added database seeding script'"
|
113
|
+
|
114
|
+
puts
|
115
|
+
say_status "Git", "Details about the application:", :yellow
|
116
|
+
puts '-'*80, ''
|
117
|
+
|
118
|
+
run "git log --reverse --pretty=format:'%Cblue%h%Creset | %s'"
|
119
|
+
|
120
|
+
puts "", "="*80
|
121
|
+
say_status "DONE", "\e[1mStarting the application...\e[0m", :yellow
|
122
|
+
puts "="*80, ""
|
123
|
+
|
124
|
+
run "rails server"
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'hashr'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'active_model'
|
5
|
+
require 'active_support/concern'
|
6
|
+
require 'active_support/configurable'
|
7
|
+
|
8
|
+
class Redis
|
9
|
+
module Persistence
|
10
|
+
include ActiveSupport::Configurable
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
include ActiveModelIntegration
|
15
|
+
self.include_root_in_json = false
|
16
|
+
|
17
|
+
def self.__redis
|
18
|
+
Redis::Persistence.config.redis
|
19
|
+
end
|
20
|
+
|
21
|
+
def __redis
|
22
|
+
self.class.__redis
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module ActiveModelIntegration
|
28
|
+
extend ActiveSupport::Concern
|
29
|
+
|
30
|
+
included do
|
31
|
+
include ActiveModel::AttributeMethods
|
32
|
+
include ActiveModel::Validations
|
33
|
+
include ActiveModel::Serialization
|
34
|
+
include ActiveModel::Serializers::JSON
|
35
|
+
include ActiveModel::Naming
|
36
|
+
include ActiveModel::Conversion
|
37
|
+
|
38
|
+
extend ActiveModel::Callbacks
|
39
|
+
define_model_callbacks :save, :destroy
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
|
45
|
+
def create(attributes={})
|
46
|
+
new(attributes).save
|
47
|
+
end
|
48
|
+
|
49
|
+
def property(name, options = {})
|
50
|
+
attr_accessor name.to_sym
|
51
|
+
properties << name.to_s unless properties.include?(name.to_s)
|
52
|
+
|
53
|
+
property_defaults[name.to_sym] = options[:default] if options[:default]
|
54
|
+
property_types[name.to_sym] = options[:class] if options[:class]
|
55
|
+
unless options[:family]
|
56
|
+
(property_families[:data] ||= []) << name.to_s
|
57
|
+
else
|
58
|
+
(property_families[options[:family].to_sym] ||= []) << name.to_s
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def properties
|
64
|
+
@properties ||= ['id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def property_defaults
|
68
|
+
@property_defaults ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def property_types
|
72
|
+
@property_types ||= {}
|
73
|
+
end
|
74
|
+
|
75
|
+
def property_families
|
76
|
+
@property_families ||= { :data => ['id'] }
|
77
|
+
end
|
78
|
+
|
79
|
+
def find(args, options={})
|
80
|
+
args.is_a?(Array) ? __find_many(args, options) : __find_one(args, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def __find_one(id, options={})
|
84
|
+
families = ['data'] | Array(options[:families])
|
85
|
+
data = __redis.hmget("#{self.model_name.plural}:#{id}", *families)
|
86
|
+
|
87
|
+
unless data.compact.empty?
|
88
|
+
attributes = data.inject({}) { |hash, item| hash.update( MultiJson.decode(item) ); hash }
|
89
|
+
self.new attributes
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def __find_all(options={})
|
94
|
+
__find_many __all_ids
|
95
|
+
end
|
96
|
+
alias :all :__find_all
|
97
|
+
|
98
|
+
def __find_many(ids, options={})
|
99
|
+
ids.map { |id| __find_one(id, options) }.compact
|
100
|
+
end
|
101
|
+
|
102
|
+
def find_each(options={}, &block)
|
103
|
+
options = { :batch_size => 1000 }.update(options)
|
104
|
+
__all_ids.each_slice options[:batch_size] do |batch|
|
105
|
+
__find_many(batch).each { |document| yield document }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def __next_id
|
110
|
+
__redis.incr("#{self.model_name.plural}_ids")
|
111
|
+
end
|
112
|
+
|
113
|
+
def __all_ids
|
114
|
+
__redis.keys("#{self.model_name.plural}:*").map { |id| id[/:(\d+)$/, 1].to_i }.sort
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
module InstanceMethods
|
120
|
+
attr_accessor :id
|
121
|
+
|
122
|
+
def initialize(attributes={})
|
123
|
+
__update_attributes self.class.property_defaults.merge(attributes)
|
124
|
+
self
|
125
|
+
end
|
126
|
+
alias :attributes= :initialize
|
127
|
+
|
128
|
+
def update_attributes(attributes={})
|
129
|
+
__update_attributes attributes
|
130
|
+
save
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
def attributes
|
135
|
+
self.class.
|
136
|
+
properties.
|
137
|
+
inject({}) {|attributes, key| attributes[key] = send(key); attributes}
|
138
|
+
end
|
139
|
+
|
140
|
+
def save
|
141
|
+
run_callbacks :save do
|
142
|
+
self.id ||= self.class.__next_id
|
143
|
+
params = self.class.property_families.keys.map do |family|
|
144
|
+
[family.to_s, self.to_json(:only => self.class.property_families[family])]
|
145
|
+
end.flatten
|
146
|
+
__redis.hmset "#{self.class.model_name.plural}:#{self.id}", *params
|
147
|
+
end
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
def destroy
|
152
|
+
run_callbacks :destroy do
|
153
|
+
__redis.del "#{self.class.model_name.plural}:#{self.id}"
|
154
|
+
end
|
155
|
+
self.freeze
|
156
|
+
end
|
157
|
+
|
158
|
+
def persisted?
|
159
|
+
__redis.exists "#{self.class.model_name.plural}:#{self.id}"
|
160
|
+
end
|
161
|
+
|
162
|
+
def inspect
|
163
|
+
"#<#{self.class}: #{attributes}>"
|
164
|
+
end
|
165
|
+
|
166
|
+
def __update_attributes(attributes)
|
167
|
+
attributes.each do |name, value|
|
168
|
+
case
|
169
|
+
when klass = self.class.property_types[name.to_sym]
|
170
|
+
if klass.is_a?(Array) && value.is_a?(Array)
|
171
|
+
send "#{name}=", value.map { |v| klass.first.new(v) }
|
172
|
+
else
|
173
|
+
send "#{name}=", klass.new(value)
|
174
|
+
end
|
175
|
+
when value.is_a?(Hash)
|
176
|
+
send "#{name}=", Hashr.new(value)
|
177
|
+
else
|
178
|
+
# Automatically convert <http://en.wikipedia.org/wiki/ISO8601> formatted strings to Time
|
179
|
+
value = Time.parse(value) if value.is_a?(String) && value =~ /^\d{4}[\/\-]\d{2}[\/\-]\d{2}T\d{2}\:\d{2}\:\d{2}Z$/
|
180
|
+
send "#{name}=", value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/redis/persistence/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Karel Minarik", "Vojtech Hyza"]
|
6
|
+
gem.email = ["karmi@karmi.cz", "vhyza@vhyza.eu"]
|
7
|
+
gem.description = %q{Simple ActiveModel-compatible persistence layer in Redis}
|
8
|
+
gem.summary = %q{Simple ActiveModel-compatible persistence layer in Redis}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "redis-persistence"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Redis::Persistence::VERSION
|
17
|
+
|
18
|
+
# = Library dependencies
|
19
|
+
#
|
20
|
+
gem.add_dependency "activemodel", "~> 3.0"
|
21
|
+
gem.add_dependency "multi_json", "~> 1.0"
|
22
|
+
gem.add_dependency "redis", "~> 2.2.2"
|
23
|
+
gem.add_dependency "hashr", "~> 0.0.16"
|
24
|
+
|
25
|
+
# = Development dependencies
|
26
|
+
#
|
27
|
+
gem.add_development_dependency "bundler", "~> 1.0"
|
28
|
+
gem.add_development_dependency "yajl-ruby", "~> 0.8.0"
|
29
|
+
gem.add_development_dependency "shoulda"
|
30
|
+
gem.add_development_dependency "mocha"
|
31
|
+
gem.add_development_dependency "turn"
|
32
|
+
end
|
data/test/_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'shoulda'
|
3
|
+
require 'turn' unless ENV["TM_FILEPATH"] || ENV["CI"]
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
|
8
|
+
require 'redis/persistence'
|
9
|
+
require 'yajl'
|
10
|
+
|
11
|
+
require 'models'
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
|
15
|
+
def setup
|
16
|
+
Redis::Persistence.config.redis = Redis.new db: ENV['REDIS_PERSISTENCE_TEST_DATABASE'] || 14
|
17
|
+
Redis::Persistence.config.redis.flushdb
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
Redis::Persistence.config.redis.flushdb
|
22
|
+
Redis::Persistence.configure do |config|
|
23
|
+
config.redis = nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require '_helper'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
class ActiveModelLintTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
include ActiveModel::Lint::Tests
|
9
|
+
|
10
|
+
def setup
|
11
|
+
super
|
12
|
+
@model = PersistentArticle.new title: 'Test'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/test/models.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
class Piece
|
2
|
+
def initialize(params)
|
3
|
+
@attributes = HashWithIndifferentAccess.new(params)
|
4
|
+
end
|
5
|
+
|
6
|
+
def method_missing(method_name, *arguments)
|
7
|
+
@attributes[method_name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def as_json(*)
|
11
|
+
@attributes
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class PersistentArticle
|
16
|
+
include Redis::Persistence
|
17
|
+
|
18
|
+
property :title
|
19
|
+
property :created
|
20
|
+
end
|
21
|
+
|
22
|
+
class ModelWithBooleans
|
23
|
+
include Redis::Persistence
|
24
|
+
|
25
|
+
property :published
|
26
|
+
property :approved
|
27
|
+
end
|
28
|
+
|
29
|
+
class ModelWithDefaults
|
30
|
+
include Redis::Persistence
|
31
|
+
|
32
|
+
property :title, default: '(Unknown)'
|
33
|
+
property :admin, default: true
|
34
|
+
end
|
35
|
+
|
36
|
+
class ModelWithCallbacks
|
37
|
+
include Redis::Persistence
|
38
|
+
|
39
|
+
before_save :my_callback_method
|
40
|
+
after_save :my_callback_method
|
41
|
+
before_destroy { @hooked = 'YEAH' }
|
42
|
+
|
43
|
+
property :title
|
44
|
+
|
45
|
+
def my_callback_method
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ModelWithValidations
|
50
|
+
include Redis::Persistence
|
51
|
+
|
52
|
+
property :title
|
53
|
+
|
54
|
+
validates_presence_of :title
|
55
|
+
end
|
56
|
+
|
57
|
+
class ModelWithCasting
|
58
|
+
|
59
|
+
class Thing
|
60
|
+
attr_reader :value
|
61
|
+
|
62
|
+
def initialize(params={})
|
63
|
+
@value = params[:value] || params['value']
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Stuff
|
68
|
+
attr_reader :values
|
69
|
+
|
70
|
+
def initialize(values)
|
71
|
+
@values = values
|
72
|
+
end
|
73
|
+
|
74
|
+
def as_json(*)
|
75
|
+
values
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
include Redis::Persistence
|
80
|
+
|
81
|
+
property :thing, :class => Thing
|
82
|
+
property :stuff, :class => Stuff, :default => []
|
83
|
+
property :pieces, :class => [Piece], :default => []
|
84
|
+
end
|
85
|
+
|
86
|
+
class ModelWithDeepHashes
|
87
|
+
include Redis::Persistence
|
88
|
+
|
89
|
+
property :tree
|
90
|
+
end
|
91
|
+
|
92
|
+
class ModelWithFamily
|
93
|
+
include Redis::Persistence
|
94
|
+
|
95
|
+
property :name
|
96
|
+
|
97
|
+
property :views, :family => 'counters'
|
98
|
+
property :visits, :family => 'counters'
|
99
|
+
|
100
|
+
property :lang, :family => 'meta'
|
101
|
+
end
|
102
|
+
|
103
|
+
class ModelWithCastingInFamily
|
104
|
+
include Redis::Persistence
|
105
|
+
property :pieces, :class => [Piece], :default => [], :family => 'meta'
|
106
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
require '_helper'
|
2
|
+
|
3
|
+
class RedisPersistenceTest < ActiveSupport::TestCase
|
4
|
+
def setup; super; end
|
5
|
+
def teardown; super; end
|
6
|
+
|
7
|
+
def in_redis
|
8
|
+
Redis::Persistence.config.redis
|
9
|
+
end
|
10
|
+
|
11
|
+
context "Redis Connection" do
|
12
|
+
|
13
|
+
should "be set" do
|
14
|
+
assert_nothing_raised { in_redis.info }
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
context "Defining properties" do
|
20
|
+
|
21
|
+
should "define accessors from attributes" do
|
22
|
+
article = PersistentArticle.new title: 'One'
|
23
|
+
assert_equal 'One', article.title
|
24
|
+
end
|
25
|
+
|
26
|
+
should "set the attributes" do
|
27
|
+
article = PersistentArticle.new title: 'One'
|
28
|
+
article.title = 'Two'
|
29
|
+
assert_equal 'Two', article.title
|
30
|
+
end
|
31
|
+
|
32
|
+
should "raise error when passing invalid attribute" do
|
33
|
+
assert_raise NoMethodError do
|
34
|
+
PersistentArticle.new krapulitzowka: 'Not'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
should "return nil for not passed attributes" do
|
39
|
+
assert_nil PersistentArticle.new.title
|
40
|
+
assert_nil PersistentArticle.create.title
|
41
|
+
end
|
42
|
+
|
43
|
+
should "return default values" do
|
44
|
+
d = ModelWithDefaults.create
|
45
|
+
assert_equal '(Unknown)', d.title
|
46
|
+
assert_equal true, d.admin
|
47
|
+
|
48
|
+
d = ModelWithDefaults.find(1)
|
49
|
+
assert_equal '(Unknown)', d.title
|
50
|
+
assert_equal true, d.admin
|
51
|
+
end
|
52
|
+
|
53
|
+
should "return time as time" do
|
54
|
+
a = PersistentArticle.create created: Time.new(2011, 11, 9).utc
|
55
|
+
assert_instance_of Time, a.created
|
56
|
+
|
57
|
+
a = PersistentArticle.find(1)
|
58
|
+
assert_instance_of Time, a.created
|
59
|
+
end
|
60
|
+
|
61
|
+
should "return boolean as boolean" do
|
62
|
+
m = ModelWithBooleans.create published: false, approved: true
|
63
|
+
assert_instance_of FalseClass, m.published
|
64
|
+
assert_instance_of TrueClass, m.approved
|
65
|
+
|
66
|
+
m = ModelWithBooleans.find(1)
|
67
|
+
assert_instance_of FalseClass, m.published
|
68
|
+
assert_instance_of TrueClass, m.approved
|
69
|
+
end
|
70
|
+
|
71
|
+
should "cast the value" do
|
72
|
+
m = ModelWithCasting.new
|
73
|
+
assert_equal [], m.stuff.values
|
74
|
+
|
75
|
+
m = ModelWithCasting.new
|
76
|
+
assert_equal [], m.pieces
|
77
|
+
|
78
|
+
m = ModelWithCasting.create thing: { :value => 1 }, stuff: [1, 2, 3], pieces: [ { name: 'One', level: 42 } ]
|
79
|
+
|
80
|
+
assert_instance_of ModelWithCasting::Thing, m.thing
|
81
|
+
assert_equal 1, m.thing.value
|
82
|
+
|
83
|
+
assert_instance_of ModelWithCasting::Stuff, m.stuff
|
84
|
+
assert_equal 1, m.stuff.values.first
|
85
|
+
|
86
|
+
assert_instance_of Array, m.pieces
|
87
|
+
assert_instance_of Piece, m.pieces.first
|
88
|
+
|
89
|
+
assert_equal 'One', m.pieces.first.name
|
90
|
+
assert_equal 42, m.pieces.first.level
|
91
|
+
|
92
|
+
m = ModelWithCasting.find(1)
|
93
|
+
assert_instance_of ModelWithCasting::Thing, m.thing
|
94
|
+
assert_instance_of ModelWithCasting::Stuff, m.stuff
|
95
|
+
assert_equal 1, m.thing.value
|
96
|
+
assert_equal 1, m.stuff.values.first
|
97
|
+
end
|
98
|
+
|
99
|
+
should "provide easy access to deep hashes" do
|
100
|
+
m = ModelWithDeepHashes.create tree: { trunk: { branch: 'leaf' } }
|
101
|
+
assert_equal 'leaf', m.tree.trunk.branch
|
102
|
+
|
103
|
+
m = ModelWithDeepHashes.find(1)
|
104
|
+
assert_equal 'leaf', m.tree.trunk.branch
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
context "Defining properties in families" do
|
110
|
+
|
111
|
+
should "store properties in the 'data' family by default" do
|
112
|
+
m = ModelWithFamily.new name: 'One'
|
113
|
+
m.save
|
114
|
+
|
115
|
+
assert in_redis.exists('model_with_families:1'), in_redis.keys.to_s
|
116
|
+
assert in_redis.hkeys('model_with_families:1').include?('data')
|
117
|
+
end
|
118
|
+
|
119
|
+
should "store properties in the correct family" do
|
120
|
+
m = ModelWithFamily.new name: 'F', views: 100, visits: 10, lang: 'en'
|
121
|
+
m.save
|
122
|
+
|
123
|
+
assert_equal 1, m.id
|
124
|
+
assert in_redis.exists('model_with_families:1'), in_redis.keys.to_s
|
125
|
+
assert in_redis.hkeys('model_with_families:1').include?('data'), in_redis.hkeys('model_with_families:1').to_s
|
126
|
+
assert in_redis.hkeys('model_with_families:1').include?('counters'), in_redis.hkeys('model_with_families:1').to_s
|
127
|
+
|
128
|
+
m = ModelWithFamily.find(1)
|
129
|
+
assert_not_nil m.name
|
130
|
+
assert_nil m.views
|
131
|
+
|
132
|
+
m = ModelWithFamily.find(1, :families => ['counters', 'meta'])
|
133
|
+
assert_not_nil m.name
|
134
|
+
assert_not_nil m.views
|
135
|
+
|
136
|
+
assert_equal 'F', m.name
|
137
|
+
assert_equal 10, m.visits
|
138
|
+
assert_equal 'en', m.lang
|
139
|
+
end
|
140
|
+
|
141
|
+
should "cast the values" do
|
142
|
+
m = ModelWithCastingInFamily.create pieces: [ { name: 'One', level: 42 } ]
|
143
|
+
m.save
|
144
|
+
|
145
|
+
m = ModelWithCastingInFamily.find(1)
|
146
|
+
assert_equal [], m.pieces
|
147
|
+
assert_nil m.pieces.first
|
148
|
+
|
149
|
+
m = ModelWithCastingInFamily.find(1, :families => 'meta')
|
150
|
+
assert_not_nil m.pieces.first
|
151
|
+
assert_equal 42, m.pieces.first.level
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
context "Class" do
|
157
|
+
|
158
|
+
should "have properties" do
|
159
|
+
assert_equal ['id', 'title', 'created'], PersistentArticle.properties
|
160
|
+
end
|
161
|
+
|
162
|
+
should "have auto-incrementing counter" do
|
163
|
+
assert_equal 1, PersistentArticle.__next_id
|
164
|
+
assert_equal 2, PersistentArticle.__next_id
|
165
|
+
end
|
166
|
+
|
167
|
+
should "create new instance" do
|
168
|
+
a = PersistentArticle.create title: 'One'
|
169
|
+
assert a.persisted?
|
170
|
+
assert_equal 1, a.id
|
171
|
+
assert in_redis.keys.size > 0, 'Key not saved into Redis?'
|
172
|
+
end
|
173
|
+
|
174
|
+
should "return one instance" do
|
175
|
+
a = PersistentArticle.create title: 'One'
|
176
|
+
a = PersistentArticle.find(1)
|
177
|
+
assert_equal 'One', a.title
|
178
|
+
end
|
179
|
+
|
180
|
+
should "return all instances" do
|
181
|
+
3.times { |i| PersistentArticle.create title: "#{i+1}" }
|
182
|
+
assert_equal 3, PersistentArticle.all.size
|
183
|
+
assert_equal '3', PersistentArticle.all.last.title
|
184
|
+
end
|
185
|
+
|
186
|
+
should "return instances by IDs" do
|
187
|
+
10.times { |i| PersistentArticle.create title: "#{i+1}" }
|
188
|
+
assert_equal 10, PersistentArticle.all.size
|
189
|
+
articles = PersistentArticle.find [2, 5, 6]
|
190
|
+
assert_equal '2', articles[0].title
|
191
|
+
assert_equal '5', articles[1].title
|
192
|
+
assert_equal '6', articles[2].title
|
193
|
+
end
|
194
|
+
|
195
|
+
should "return instances in batches" do
|
196
|
+
runs = 0
|
197
|
+
100.times { |i| PersistentArticle.create title: "#{i+1}" }
|
198
|
+
|
199
|
+
PersistentArticle.find_each :batch_size => 10 do |article|
|
200
|
+
article.title += ' (touched)' and article.save
|
201
|
+
runs += 1
|
202
|
+
end
|
203
|
+
|
204
|
+
assert_equal 100, runs
|
205
|
+
assert_match /touched/, PersistentArticle.find(1).title
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
context "Instance" do
|
211
|
+
|
212
|
+
should "be inspectable" do
|
213
|
+
assert_nothing_raised do
|
214
|
+
assert_match /PersistentArticle/, PersistentArticle.new(id: 1, title: { some: { deep: 'Hash', :a => [1, 2, 3] } }).inspect
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
should "have attributes" do
|
219
|
+
assert_equal ['id', 'title', 'created'], PersistentArticle.new.attributes.keys
|
220
|
+
end
|
221
|
+
|
222
|
+
should "not persist by default" do
|
223
|
+
assert ! PersistentArticle.new.persisted?
|
224
|
+
end
|
225
|
+
|
226
|
+
should "be saved and found in Redis" do
|
227
|
+
article = PersistentArticle.new id: 1, title: 'One'
|
228
|
+
assert article.save
|
229
|
+
assert in_redis.exists("persistent_articles:1")
|
230
|
+
|
231
|
+
assert PersistentArticle.find(1)
|
232
|
+
assert in_redis.keys.size > 0, 'Key not saved into Redis?'
|
233
|
+
assert_equal 'One', PersistentArticle.find(1).title
|
234
|
+
end
|
235
|
+
|
236
|
+
should "be deleted from Redis" do
|
237
|
+
article = PersistentArticle.new id: 1, title: 'One'
|
238
|
+
assert article.save
|
239
|
+
assert_not_nil PersistentArticle.find(1)
|
240
|
+
assert in_redis.keys.size > 0, 'Key not saved into Redis?'
|
241
|
+
|
242
|
+
article.destroy
|
243
|
+
assert_nil PersistentArticle.find(1)
|
244
|
+
assert_equal 0, in_redis.keys.size, 'Key not removed from Redis?'
|
245
|
+
end
|
246
|
+
|
247
|
+
should "update attributes" do
|
248
|
+
a = PersistentArticle.new title: 'Old'
|
249
|
+
a.save
|
250
|
+
|
251
|
+
assert a.update_attributes title: 'New'
|
252
|
+
|
253
|
+
a = PersistentArticle.find(1)
|
254
|
+
assert_equal 'New', a.title
|
255
|
+
end
|
256
|
+
|
257
|
+
should "get auto-incrementing ID on save when none is passed" do
|
258
|
+
article = PersistentArticle.new title: 'One'
|
259
|
+
|
260
|
+
assert_nil article.id
|
261
|
+
|
262
|
+
assert article.save
|
263
|
+
assert_not_nil article.id
|
264
|
+
|
265
|
+
assert_equal 1, PersistentArticle.find(1).id
|
266
|
+
assert_equal 2, PersistentArticle.__next_id
|
267
|
+
end
|
268
|
+
|
269
|
+
should "fire before_save hooks" do
|
270
|
+
article = ModelWithCallbacks.new title: 'Hooks'
|
271
|
+
article.expects(:my_callback_method).twice
|
272
|
+
|
273
|
+
article.save
|
274
|
+
end
|
275
|
+
|
276
|
+
should "fire before_destroy hooks" do
|
277
|
+
article = ModelWithCallbacks.new title: 'Hooks'
|
278
|
+
article.save
|
279
|
+
article.destroy
|
280
|
+
|
281
|
+
assert_equal 'YEAH', article.instance_variable_get(:@hooked)
|
282
|
+
end
|
283
|
+
|
284
|
+
should "perform validations" do
|
285
|
+
m = ModelWithValidations.new
|
286
|
+
|
287
|
+
assert ! m.valid?
|
288
|
+
assert_equal 1, m.errors.to_a.size
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis-persistence
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Karel Minarik
|
9
|
+
- Vojtech Hyza
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-11-10 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activemodel
|
17
|
+
requirement: &70276358101260 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70276358101260
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: multi_json
|
28
|
+
requirement: &70276358100760 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70276358100760
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: redis
|
39
|
+
requirement: &70276358100300 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 2.2.2
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70276358100300
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: hashr
|
50
|
+
requirement: &70276358099840 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.0.16
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *70276358099840
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: bundler
|
61
|
+
requirement: &70276358099380 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '1.0'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *70276358099380
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: yajl-ruby
|
72
|
+
requirement: &70276358098920 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.8.0
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *70276358098920
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: shoulda
|
83
|
+
requirement: &70276358098540 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: *70276358098540
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: mocha
|
94
|
+
requirement: &70276352583600 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: *70276352583600
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: turn
|
105
|
+
requirement: &70276352974040 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: *70276352974040
|
114
|
+
description: Simple ActiveModel-compatible persistence layer in Redis
|
115
|
+
email:
|
116
|
+
- karmi@karmi.cz
|
117
|
+
- vhyza@vhyza.eu
|
118
|
+
executables: []
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files: []
|
121
|
+
files:
|
122
|
+
- .gitignore
|
123
|
+
- Gemfile
|
124
|
+
- README.markdown
|
125
|
+
- Rakefile
|
126
|
+
- examples/article.rb
|
127
|
+
- examples/benchmark.rb
|
128
|
+
- examples/rails-template.rb
|
129
|
+
- lib/redis/persistence.rb
|
130
|
+
- lib/redis/persistence/version.rb
|
131
|
+
- redis-persistence.gemspec
|
132
|
+
- test/_helper.rb
|
133
|
+
- test/active_model_lint_test.rb
|
134
|
+
- test/models.rb
|
135
|
+
- test/redis_persistence_test.rb
|
136
|
+
homepage: ''
|
137
|
+
licenses: []
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ! '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 1.8.10
|
157
|
+
signing_key:
|
158
|
+
specification_version: 3
|
159
|
+
summary: Simple ActiveModel-compatible persistence layer in Redis
|
160
|
+
test_files:
|
161
|
+
- test/_helper.rb
|
162
|
+
- test/active_model_lint_test.rb
|
163
|
+
- test/models.rb
|
164
|
+
- test/redis_persistence_test.rb
|