redis-persistence 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ scratch/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redis-persistence.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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,5 @@
1
+ class Redis
2
+ module Persistence
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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
@@ -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
@@ -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