redis_orm 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Dmitrii Samoilov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ Manifest
3
+ README.md
4
+ Rakefile
5
+ lib/redis_orm.rb
6
+ lib/redis_orm/active_model_behavior.rb
7
+ lib/redis_orm/associations/belongs_to.rb
8
+ lib/redis_orm/associations/has_many.rb
9
+ lib/redis_orm/associations/has_many_proxy.rb
10
+ lib/redis_orm/associations/has_one.rb
11
+ lib/redis_orm/redis_orm.rb
12
+ redis_orm.gemspec
13
+ test/associations_test.rb
14
+ test/basic_functionality_test.rb
15
+ test/callbacks_test.rb
16
+ test/changes_array_test.rb
17
+ test/dynamic_finders_test.rb
18
+ test/exceptions_test.rb
19
+ test/has_one_has_many_test.rb
20
+ test/indices_test.rb
21
+ test/options_test.rb
22
+ test/redis.conf
23
+ test/validations_test.rb
data/README.md ADDED
@@ -0,0 +1,332 @@
1
+ RedisOrm supposed to be *almost* drop-in replacement of ActiveRecord. It's based on the [Redis](http://redis.io) advanced key-value store and is work in progress.
2
+
3
+ Here's the standard model definition:
4
+
5
+ ```ruby
6
+ class User < RedisOrm::Base
7
+ property :first_name, String
8
+ property :last_name, String
9
+ property :created_at, Time
10
+ property :modified_at, Time
11
+
12
+ index :last_name
13
+ index [:first_name, :last_name]
14
+
15
+ has_many :photos
16
+ has_one :profile
17
+
18
+ after_create :create_mailboxes
19
+
20
+ def create_mailboxes
21
+ # ...
22
+ end
23
+ end
24
+ ```
25
+
26
+ ## Defining a model and specifing properties
27
+
28
+ To specify properties for your model you should use the following syntax:
29
+
30
+ ```ruby
31
+ class User < RedisOrm::Base
32
+ property :first_name, String
33
+ property :last_name, String
34
+ property :created_at, Time
35
+ property :modified_at, Time
36
+ end
37
+ ```
38
+
39
+ Following property types are supported:
40
+
41
+ * **Integer**
42
+
43
+ * **String**
44
+
45
+ * **Float**
46
+
47
+ * **RedisOrm::Boolean**
48
+ there is no Boolean class in Ruby so it's a special class to specify TrueClass or FalseClass objects
49
+
50
+ * **Time**
51
+
52
+ The value of the
53
+ Property definition supports following options:
54
+
55
+ * **:default**
56
+
57
+ The default value of the attribute when it's getting saved w/o any.
58
+
59
+ ## Searching records by the value
60
+
61
+ Usually it's done via specifing an index and using dynamic finders. For example:
62
+
63
+ ```ruby
64
+ class User < RedisOrm::Base
65
+ property :name, String
66
+
67
+ index :name
68
+ end
69
+
70
+ User.create :name => "germaninthetown"
71
+ User.find_by_name "germaninthetown" # => found 1 record
72
+ User.find_all_by_name "germaninthetown" # => array with 1 record
73
+ ```
74
+
75
+ Dynamic finders work mostly the way they work in ActiveRecord. The only difference is if you didn't specified index or compaund index on the attributes you are searching on the exception will be raised.
76
+
77
+ ## Options for #find/#all
78
+
79
+ For example we associate 2 photos with the album
80
+
81
+ ```ruby
82
+ @album.photos << @photo2
83
+ @album.photos << @photo1
84
+ ```
85
+
86
+ To extract all or part of the associated records by far you could use 3 options (#find is an alias for #all in has_many proxy):
87
+
88
+ ```ruby
89
+ @album.photos.all(:limit => 0, :offset => 0).should == []
90
+ @album.photos.all(:limit => 1, :offset => 0).size.should == 1
91
+ @album.photos.all(:limit => 2, :offset => 0)
92
+ @album.photos.find(:order => "asc")
93
+
94
+ Photo.all(:order => "asc", :limit => 5)
95
+ Photo.all(:order => "desc", :limit => 10, :offset => 50)
96
+ ```
97
+
98
+ ## Indices
99
+
100
+ Indices are used in a different way then they are used in relational databases.
101
+
102
+ You could add index to any attribute of the model (it also could be compound):
103
+
104
+ ```ruby
105
+ class User < RedisOrm::Base
106
+ property :first_name, String
107
+ property :last_name, String
108
+
109
+ index :first_name
110
+ index [:first_name, :last_name]
111
+ end
112
+ ```
113
+
114
+ With index defined for the property (or number of properties) the id of the saved object is stored in the special sorted set, so it could be found later. For example with defined User model for the above code:
115
+
116
+ ```ruby
117
+ user = User.new :first_name => "Robert", :last_name => "Pirsig"
118
+ user.save
119
+
120
+ # 2 redis keys are created "user:first_name:Robert" and "user:first_name:Robert:last_name:Pirsig" so we could search things like this:
121
+
122
+ User.find_by_first_name("Robert") # => user
123
+ User.find_all_by_first_name("Robert") # => [user]
124
+ User.find_by_first_name_and_last_name("Robert", "Pirsig") # => user
125
+ User.find_all_by_first_name_and_last_name("Chris", "Pirsig") # => []
126
+ ```
127
+
128
+ Index definition supports following options:
129
+
130
+ * **:unique** Boolean default: false
131
+
132
+ ## Associations
133
+
134
+ RedisOrm provides 3 association types:
135
+
136
+ * has_one
137
+
138
+ * has_many
139
+
140
+ * belongs_to
141
+
142
+ HABTM association could be emulated with 2 has_many declarations in related models.
143
+
144
+ ### has_many/belongs_to associations
145
+
146
+ ```ruby
147
+ class Article < RedisOrm::Base
148
+ property :title, String
149
+ has_many :comments
150
+ end
151
+
152
+ class Comment < RedisOrm::Base
153
+ property :body, String
154
+ belongs_to :article
155
+ end
156
+
157
+ article = Article.create :title => "DHH drops OpenID support on 37signals"
158
+ comment1 = Comment.create :body => "test"
159
+ comment2 = Comment.create :body => "test #2"
160
+
161
+ article.comments << [comment1, comment2]
162
+
163
+ # or rewrite associations
164
+ article.comments = [comment1, comment2]
165
+
166
+ article.comments # => [comment1, comment2]
167
+ comment1.article # => article
168
+ comment2.article # => article
169
+ ```
170
+
171
+ Backlinks are automatically created.
172
+
173
+ ### has_one/belongs_to associations
174
+
175
+ ```ruby
176
+ class User < RedisOrm::Base
177
+ property :name, String
178
+ has_one :profile
179
+ end
180
+
181
+ class Profile < RedisOrm::Base
182
+ property :age, Integer
183
+
184
+ validates_presence_of :age
185
+ belongs_to :user
186
+ end
187
+
188
+ user = User.create :name => "Haruki Murakami"
189
+ profile = Profile.create :age => 26
190
+ user.profile = profile
191
+
192
+ user.profile # => profile
193
+ profile.user # => user
194
+ ```
195
+
196
+ Backlink is automatically created.
197
+
198
+ ### has_many/has_many associations (HABTM)
199
+
200
+ ```ruby
201
+ class Article < RedisOrm::Base
202
+ property :title, String
203
+ has_many :categories
204
+ end
205
+
206
+ class Category < RedisOrm::Base
207
+ property :name, String
208
+ has_many :articles
209
+ end
210
+
211
+ article = Article.create :title => "DHH drops OpenID support on 37signals"
212
+
213
+ cat1 = Category.create :name => "Nature"
214
+ cat2 = Category.create :name => "Art"
215
+ cat3 = Category.create :name => "Web"
216
+
217
+ article.categories << [cat1, cat2, cat3]
218
+
219
+ article.categories # => [cat1, cat2, cat3]
220
+ cat1.articles # => [article]
221
+ cat2.articles # => [article]
222
+ cat3.articles # => [article]
223
+ ```
224
+
225
+ Backlinks are automatically created.
226
+
227
+ ### self-referencing association
228
+
229
+ ```ruby
230
+ class User < RedisOrm::Base
231
+ property :name, String
232
+ index :name
233
+ has_many :users, :as => :friends
234
+ end
235
+
236
+ me = User.create :name => "german"
237
+ friend1 = User.create :name => "friend1"
238
+ friend2 = User.create :name => "friend2"
239
+
240
+ me.friends << [friend1, friend2]
241
+
242
+ me.friends # => [friend1, friend2]
243
+ friend1.friends # => []
244
+ friend2.friends # => []
245
+ ```
246
+
247
+ As an exception if *:as* option for the association is provided the backlinks are not established.
248
+
249
+ For more examples please check test/associations_test.rb
250
+
251
+ ## Validation
252
+
253
+ RedisOrm includes ActiveModel::Validations. So all well-known functions are already in. An example from test/validations_test.rb:
254
+
255
+ ```ruby
256
+ class Photo < RedisOrm::Base
257
+ property :image, String
258
+
259
+ validates_presence_of :image
260
+ validates_length_of :image, :in => 7..32
261
+ validates_format_of :image, :with => /\w*\.(gif|jpe?g|png)/
262
+ end
263
+ ```
264
+
265
+ ## Callbacks
266
+
267
+ RedisOrm provides 6 standard callbacks:
268
+
269
+ ```ruby
270
+ after_save :callback
271
+ before_save :callback
272
+ after_create :callback
273
+ before_create :callback
274
+ after_destroy :callback
275
+ before_destroy :callback
276
+ ```
277
+
278
+ They are implemented differently than in ActiveModel though work as expected:
279
+
280
+ ```ruby
281
+ class Comment < RedisOrm::Base
282
+ property :text, String
283
+
284
+ belongs_to :user
285
+
286
+ before_save :trim_whitespaces
287
+
288
+ def trim_whitespaces
289
+ self.text = self.text.strip
290
+ end
291
+ end
292
+ ```
293
+
294
+ ## Saving records
295
+
296
+ When saving object standard ActiveModel's #valid? method is invoked at first. Then appropriate callbacks are run. Then new Hash in Redis is created with keys/values equal to the properties/values of the saving object.
297
+
298
+ The object's id is stored in "model_name:ids" sorted set with Time.now.to_f as a score. So records are ordered by created_at time by default.
299
+
300
+ ## Tests
301
+
302
+ Though I a fan of the Test::Unit all tests are based on RSpec. And the only reason I did it is before(:all) and after(:all) hooks. So I could spawn/kill redis-server's process:
303
+
304
+ ```ruby
305
+ describe "check callbacks" do
306
+ before(:all) do
307
+ path_to_conf = File.dirname(File.expand_path(__FILE__)) + "/redis.conf"
308
+ $redis_pid = spawn 'redis-server ' + path_to_conf, :out => "/dev/null"
309
+ sleep(0.3) # must be some delay otherwise "Connection refused - Unable to connect to Redis"
310
+ path_to_socket = File.dirname(File.expand_path(__FILE__)) + "/../redis.sock"
311
+ $redis = Redis.new(:host => 'localhost', :path => path_to_socket)
312
+ end
313
+
314
+ before(:each) do
315
+ $redis.flushall if $redis
316
+ end
317
+
318
+ after(:each) do
319
+ $redis.flushall if $redis
320
+ end
321
+
322
+ after(:all) do
323
+ Process.kill 9, $redis_pid.to_i if $redis_pid
324
+ end
325
+
326
+ # it "should ..." do
327
+ # ...
328
+ # end
329
+ end
330
+ ```
331
+
332
+ Copyright © 2011 Dmitrii Samoilov, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'echoe'
5
+
6
+ Echoe.new('redis_orm', '0.1') do |p|
7
+ p.description = "ORM for Redis advanced key-value storage"
8
+ p.url = "https://github.com/german/redis_orm"
9
+ p.author = "Dmitrii Samoilov"
10
+ p.email = "germaninthetown@gmail.com"
11
+ p.dependencies = ["activesupport >=3.0.0", "activemodel >=3.0.0", "redis >=2.2.0"]
12
+ p.development_dependencies = ["rspec >=2.5.0"]
13
+ end
14
+
15
+
16
+ #require 'rake/testtask'
17
+ #$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
18
+
19
+ =begin
20
+ desc 'Test the redis_orm gem.'
21
+ Rake::TestTask.new(:test) do |t|
22
+ #t.libs << 'lib'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = true
25
+ end
26
+ =end
27
+
28
+ task :test do |t|
29
+ Dir['test/**/*_test.rb'].each do |file|
30
+ puts `ruby -I./lib #{file}`
31
+ end
32
+ end
data/lib/redis_orm.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'active_model'
2
+ require 'redis'
3
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'active_model_behavior')
4
+
5
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'belongs_to')
6
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_many_proxy')
7
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_many')
8
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'associations', 'has_one')
9
+
10
+ class String
11
+ def i18n_key
12
+ self.to_s.tableize
13
+ end
14
+
15
+ def human
16
+ self.to_s.humanize
17
+ end
18
+ end
19
+
20
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'redis_orm', 'redis_orm')
@@ -0,0 +1,20 @@
1
+ module ActiveModelBehavior
2
+ module ClassMethods
3
+ def model_name
4
+ #@_model_name ||= ActiveModel::Name.new(self).to_s.downcase
5
+ @_model_name ||= ActiveModel::Name.new(self).to_s.tableize.singularize
6
+ end
7
+ end
8
+
9
+ module InstanceMethods
10
+ def model_name
11
+ #@_model_name ||= ActiveModel::Name.new(self.class).to_s.downcase
12
+ @_model_name ||= ActiveModel::Name.new(self.class).to_s.tableize.singularize
13
+ end
14
+ end
15
+
16
+ def self.included(base)
17
+ base.send(:include, InstanceMethods)
18
+ base.extend ClassMethods
19
+ end
20
+ end