redisrecord 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,23 @@
1
+ Copyright (c) 2009 Mauro Pompilio
2
+
3
+ Permission is hereby granted, free of charge, to any
4
+ person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the
6
+ Software without restriction, including without limitation
7
+ the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the
9
+ Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice
13
+ shall be included in all copies or substantial portions of
14
+ the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
19
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
20
+ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
21
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ # RedisRecord
2
+ A "virtual" Object Relational Mapper on top of [Redis](http://redis.googlecode.com).
3
+
4
+ This is a proof-of-concept. Allows you to create schema-less data structures and <br/>
5
+ build relationships between them, using Redis as storage.
6
+
7
+ ## Author
8
+ Mauro Pompilio <hackers.are.rockstars@gmail.com>
9
+
10
+ ## License
11
+ MIT
12
+
13
+ ## Example
14
+
15
+ class User < RedisRecord::Base
16
+ database 15
17
+ has_many :posts
18
+ end
19
+
20
+ class Post < RedisRecord::Base
21
+ database 15
22
+ belongs_to :user
23
+ has_many :comments
24
+ end
25
+
26
+ class Comment < RedisRecord::Base
27
+ database 15
28
+ belongs_to :post
29
+ belongs_to :user
30
+ end
31
+
32
+ >> u = User.new
33
+ => #<User:0xb761c3f0 @stored_attrs=#<Set: {}>, @cached_attrs={}, @opts={}>
34
+ >> u.name = 'Mauro'
35
+ => "Mauro"
36
+ >> u.age = 25
37
+ => 25
38
+ >> u.save
39
+ => [:updated_at, :age, :name, :id, :created_at]
40
+ >> u.whatever = {:as_many_attributes => 'as you want'}
41
+ => {:as_many_attributes=>"as you want"}
42
+ >> u.save
43
+ => [:updated_at, :whatever]
44
+ >> u
45
+ => #<User:0xb761c3f0 @stored_attrs=#<Set: {:updated_at, :age, :name, :whatever, :id, :created_at}>, @cached_attrs={:updated_at=>"1238173522.49843", :age=>25, :name=>"Mauro", :whatever=>{:as_many_attributes=>"as you want"}, :id=>1, :created_at=>"1238173522.49808"}, @opts={}>
46
+ >> u = User.find(1)
47
+ => #<User:0xb760a2a4 @stored_attrs=#<Set: {:updated_at, :age, :name, :whatever, :id, :created_at}>, @cached_attrs={:updated_at=>"1238173566", :age=>"25", :name=>"Mauro", :whatever=>"as_many_attributesas you want", :id=>"1", :created_at=>"1238173522.49808"}, @opts={:stored=>true}>
48
+ >> p = Post.new
49
+ => #<Post:0xb7608210 @stored_attrs=#<Set: {}>, @cached_attrs={:user_id=>nil}, @opts={}>
50
+ >> p.title = 'New Post'
51
+ => "New Post"
52
+ >> p.user_id = u.id
53
+ => "1"
54
+ >> p.save
55
+ => [:updated_at, :title, :id, :user_id, :created_at:
56
+ >> p2 = Post.new
57
+ => #<Post:0xb75e5ad0 @stored_attrs=#<Set: {}>, @cached_attrs={:user_id=>nil}, @opts={}>
58
+ >> p2.title = 'Another post'
59
+ => "Another post"
60
+ >> p2.user_id = u.id
61
+ => "1"
62
+ >> p2.save
63
+ => [:updated_at, :title, :id, :user_id, :created_at]
64
+ >> p.user
65
+ => #<User:0xb75ce754 @stored_attrs=#<Set: {:updated_at, :age, :name, :whatever, :id, :created_at}>, @cached_attrs={:updated_at=>"1238173566", :age=>"25", :name=>"Mauro", :whatever=>"as_many_attributesas you want", :id=>"1", :created_at=>"1238173522.49808"}, @opts={:stored=>true}>
66
+ >> p2.user
67
+ => #<User:0xb75c8480 @stored_attrs=#<Set: {:updated_at, :age, :name, :whatever, :id, :created_at}>, @cached_attrs={:updated_at=>"1238173566", :age=>"25", :name=>"Mauro", :whatever=>"as_many_attributesas you want", :id=>"1", :created_at=>"1238173522.49808"}, @opts={:stored=>true}>
68
+ >> u.posts
69
+ => [#<Post:0xb75c20a8 @stored_attrs=#<Set: {:updated_at, :title, :id, :user_id, :created_at}>, @cached_attrs={:updated_at=>"1238173641", :title=>"New Post", :id=>"1", :user_id=>"1", :created_at=>"1238173641.17936"}, @opts={:stored=>true}>, #<Post:0xb75bdb0c @stored_attrs=#<Set: {:updated_at, :title, :id, :user_id, :created_at}>, @cached_attrs={:updated_at=>"1238173858", :title=>"Another post", :id=>"2", :user_id=>"1", :created_at=>"1238173858.82325"}, @opts={:stored=>true}>]
@@ -0,0 +1,60 @@
1
+ # = Sample file
2
+ require 'lib/redisrecord'
3
+
4
+ # example.rb demo class
5
+ class User < RedisRecord::Model
6
+ database 0
7
+ has_many :posts
8
+ #has_one :moderator
9
+ end
10
+
11
+ # example.rb demo class
12
+ class Post < RedisRecord::Model
13
+ database 1
14
+ belongs_to :user
15
+ has_many :comments
16
+ has_and_belongs_to_many :categories
17
+ end
18
+
19
+ # example.rb demo class
20
+ class Comment < RedisRecord::Model
21
+ database 15
22
+ belongs_to :post
23
+ belongs_to :user
24
+ end
25
+
26
+ # example.rb demo class
27
+ class Category < RedisRecord::Model
28
+ has_and_belongs_to_many :posts
29
+ end
30
+
31
+ # example.rb demo class
32
+ class Moderator < RedisRecord::Model
33
+ database 15
34
+ belongs_to :user
35
+ end
36
+
37
+ # Example
38
+ #p u = User.new
39
+ #p u.name = 'Mauro'
40
+ #p u.age = 25
41
+ #p u.save
42
+ #p u.whatever = {:as_many_attributes => 'as you want'}
43
+ #p u.save
44
+ #p u
45
+ #p u = User.find(1)
46
+ #p p = Post.new
47
+ #p p.title = 'New Post'
48
+ #p p.user_id = u.id
49
+ #p p.save
50
+ #p p2 = Post.new
51
+ #p p2.title = 'Another post'
52
+ #p p2.user_id = u.id
53
+ #p p2.save
54
+ #p p.user
55
+ #p p2.user
56
+ #p u.posts
57
+
58
+ # Flush DB
59
+ #r = Redis.new(:db => 15)
60
+ #r.flush_db
@@ -0,0 +1,45 @@
1
+ #
2
+ # Sphinx configuration file sample for RedisRecord
3
+ #
4
+
5
+ # data source definition
6
+ source redisrecord
7
+ {
8
+ type= xmlpipe2
9
+ xmlpipe_command= cat /tmp/redisindex.xml
10
+ }
11
+
12
+ # index definition
13
+ index redisindex
14
+ {
15
+ source= redisrecord
16
+ path= /tmp/redisindex
17
+ docinfo= extern
18
+ mlock= 0
19
+ morphology= none
20
+ min_word_len= 1
21
+ charset_type= utf-8
22
+ html_strip= 0
23
+ }
24
+
25
+ # indexer settings
26
+ indexer
27
+ {
28
+ mem_limit= 32M
29
+ }
30
+
31
+ # searchd settings
32
+ searchd
33
+ {
34
+ port= 3312
35
+ log= /tmp/searchd.log
36
+ query_log= /tmp/query.log
37
+ read_timeout= 5
38
+ max_children= 30
39
+ pid_file= /tmp/searchd.pid
40
+ max_matches= 1000
41
+ seamless_rotate= 1
42
+ preopen_indexes= 0
43
+ unlink_old= 1
44
+ }
45
+ # --eof--
@@ -0,0 +1,9 @@
1
+ one:
2
+ name: "Mauro"
3
+ lastname: "Pompilio"
4
+ age: 25
5
+
6
+ two:
7
+ name: "Ivan"
8
+ lastname: "Belmonte"
9
+ age: 30
@@ -0,0 +1,374 @@
1
+ # = RedisRecord
2
+ # A "virtual" Object Relational Mapper on top of Redis[http://redis.googlecode.com].
3
+ #
4
+ # This is a proof-of-concept. Allows you to create schema-less data structures and
5
+ # build relationships between them, using Redis as storage.
6
+ #
7
+ # == Main repository
8
+ # http://github.com/malditogeek/redisrecord/tree/master
9
+ #
10
+ # == Author
11
+ # Mauro Pompilio <hackers.are.rockstars@gmail.com>
12
+ #
13
+ # == License
14
+ # MIT
15
+ #
16
+
17
+ require 'activesupport'
18
+ require 'redis'
19
+
20
+ # = RedisRecord
21
+ module RedisRecord
22
+
23
+ # Not found exception.
24
+ class RecordNotFound < Exception; end
25
+
26
+ # Duplicate attribute exception.
27
+ class DuplicateAttribute < Exception; end
28
+
29
+ # Connection to Redis.
30
+ class RedisConnection < Redis; end
31
+
32
+ # Base class.
33
+ class Model
34
+ attr_reader :attrs, :stored_attrs
35
+
36
+ # Redis connection
37
+ @@redis = RedisConnection.new(:logger => Logger.new(STDOUT))
38
+
39
+ # Reflections
40
+ @@reflections = {}
41
+
42
+ # Class methods
43
+ class << self
44
+
45
+ # Generates reflections skeleton when the RedisRecord is inherited.
46
+ def inherited(klass)
47
+ @@reflections[klass.name.to_sym] = {}
48
+ [:belongs_to, :has_many].each do |r|
49
+ @@reflections[klass.name.to_sym][r] = []
50
+ end
51
+ end
52
+
53
+ # Retrieve records. Allowed:
54
+ # * :all
55
+ # * :first
56
+ # * :last
57
+ # * one or more IDs.
58
+ #
59
+ # Example:
60
+ # Post.find(:last)
61
+ # Post.find(1)
62
+ # Post.find([1,2,3])
63
+ #
64
+ # Allowed options:
65
+ # * :should_raise: If *true* raise RecordNotFound exception on missing records. Defauls is *false*.
66
+ def find(*args)
67
+ options = args.last.is_a?(Hash) ? args.pop : {}
68
+ case args.first
69
+ when :all
70
+ find_all(options)
71
+ when :first
72
+ find_first(options)
73
+ when :last
74
+ find_last(options)
75
+ else
76
+ find_from_ids(args, options)
77
+ end
78
+ end
79
+
80
+ # Sort class objects by the given attribute. Defaults are:
81
+ # * :order => 'ALPHA DESC'
82
+ # * :limit => 10
83
+ #
84
+ # Example:
85
+ # Post.sort_by(:updated_at) #=> Last 10 updated posts
86
+ # Post.sort_by(:id, :order => 'ALPHA ASC', :limit => 5) #=> First 5 posts
87
+ def sort_by(attribute, options={})
88
+ limit = options[:limit] || 10
89
+ offset = options[:offset] || 0
90
+ order = options[:order] || 'ALPHA DESC'
91
+ ids = @@redis.sort("#{name}:list",
92
+ :by => "#{name}:*:#{attribute}",
93
+ :limit => [offset, limit],
94
+ :order => order,
95
+ :get => "#{name}:*:id")
96
+ find_from_ids(ids, options)
97
+ end
98
+
99
+ def _connection
100
+ @@redis
101
+ end
102
+
103
+ def get_lookup(attr, value)
104
+ _connection.get("#{name}:lookup:#{attr}:#{value}")
105
+ end
106
+
107
+ private # class methods
108
+
109
+ # Captures the *class* missing methods.
110
+ def method_missing(*args)
111
+ case args[0].to_s
112
+ when /^find_by_/
113
+ lookup = args[0].to_s.gsub(/^find_by_/, '')
114
+ find(get_lookup(lookup, args[1]))
115
+ end
116
+ end
117
+
118
+ def find_all(options)
119
+ all_ids = @@redis.list_range("#{name}:list", 0, -1)
120
+ [find_from_ids(all_ids, options)].flatten
121
+ end
122
+
123
+ def find_first(options)
124
+ first_id = @@redis.list_index("#{name}:list", 0)
125
+ find_from_ids(first_id, options)
126
+ end
127
+
128
+ def find_last(options)
129
+ first_id = @@redis.list_index("#{name}:list", -1)
130
+ find_from_ids(first_id, options)
131
+ end
132
+
133
+ def find_from_ids(ids, options={})
134
+ records = []
135
+ ids = [ids].flatten
136
+ ids.each do |id|
137
+ begin
138
+ redis_attrs = @@redis.set_members("#{name}:#{id}:attrs").to_a
139
+ raise if redis_attrs.empty?
140
+ stored_attrs = @@redis.mget(redis_attrs.map {|a| "#{name}:#{id}:#{a}"})
141
+ object_attrs = {}
142
+ redis_attrs.each_with_index {|a,i| object_attrs[a.to_sym] = stored_attrs[i]}
143
+ records << instantiate(object_attrs)
144
+ rescue
145
+ raise RecordNotFound.new("#{name}:#{id}") if options[:should_raise]
146
+ #records << nil
147
+ end
148
+ end
149
+ (ids.length == 1 ? records[0] : records)
150
+ end
151
+
152
+ # Select which database to be used.
153
+ # class Post < RedisRecord
154
+ # database 15
155
+ # end
156
+ def database(db)
157
+ puts 'Per class database selection is DISABLED.'
158
+ return false
159
+ #_connection.call_command ['select', db]
160
+ end
161
+
162
+ # Returns a new object with the given attributes hash.
163
+ def instantiate(instance_attrs={})
164
+ new(instance_attrs, :stored => true)
165
+ end
166
+
167
+ # Belongs_to relationship initialization.
168
+ # class Post < RedisRecord
169
+ # belongs_to :user
170
+ # end
171
+ def belongs_to(*klasses)
172
+ klasses.each do |klass|
173
+ add_reflection :belongs_to, klass
174
+
175
+ fkey = klass.to_s.foreign_key.to_sym
176
+ define_method("#{klass}") do
177
+ k = klass.to_s.classify.constantize
178
+ k.find(@cached_attrs[fkey])
179
+ end
180
+ define_method("#{klass}=") do |new_value|
181
+ k = klass.to_s.classify.constantize
182
+ if new_value.is_a?(k)
183
+ @cached_attrs[fkey] = new_value.id
184
+ k.find(new_value.id)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # Has_many relationship initialization.
191
+ # class Post < RedisRecord
192
+ # has_many :comments
193
+ # end
194
+ def has_many(*klasses)
195
+ klasses.each do |klass|
196
+ add_reflection :has_many, klass
197
+
198
+ define_method("#{klass}") {
199
+ k = klass.to_s.classify.constantize
200
+ ids = @@redis.set_members("#{self.class.name}:#{@cached_attrs[:id]}:_#{klass.to_s.singularize}_ids").to_a
201
+ ids.empty? ? [] : [k.find(ids)].flatten
202
+ }
203
+ end
204
+ end
205
+
206
+ # Has_one relationship initialization.
207
+ # class Post < RedisRecord
208
+ # has_one :permalink
209
+ # end
210
+ #def has_one(klass)
211
+ # add_reflection :has_one, klass
212
+ # define_method("#{klass}") {
213
+ # k = klass.to_s.classify.constantize
214
+ # k.find(@@redis["#{self.class.name}:#{@cached_attrs[:id]}:_#{klass.to_s.singularize}_id"])
215
+ # }
216
+ #end
217
+
218
+ # Has_and_belongs_to_many relationship initialization.
219
+ def has_and_belongs_to_many(klass)
220
+ has_many klass
221
+ belongs_to klass.to_s.singularize
222
+ end
223
+
224
+ def add_reflection(reflection, klass)
225
+ @@reflections[self.name.to_sym][reflection] << klass
226
+ end
227
+
228
+ end # Class methods
229
+
230
+ # Instantiate a new object with the given *attrs* hash.
231
+ def initialize(attrs={},opts={})
232
+ @opts = opts.freeze
233
+
234
+ # Object attrs
235
+ @cached_attrs, @stored_attrs = {}, Set.new
236
+ add_attributes attrs
237
+ attrs.keys.each {|k| @stored_attrs << k.to_sym} if @opts[:stored]
238
+ add_foreign_keys_as_attributes
239
+ end
240
+
241
+ # Save the (non-stored) object attributes to Redis.
242
+ def save
243
+ # Autoincremental ID unless specified
244
+ unless @cached_attrs.include?(:id)
245
+ add_attribute :id, @@redis.incr("#{self.class.name}:autoincrement")
246
+ add_attribute :created_at, Time.now.to_f.to_s
247
+ add_attribute :updated_at, Time.now.to_f.to_s
248
+ @@redis.push_tail("#{self.class.name}:list", @cached_attrs[:id]) # List of all the class objects
249
+ end
250
+
251
+ # Store each @cached_attrs
252
+ stored = Set.new
253
+ (@cached_attrs.keys - @stored_attrs.to_a).each do |k|
254
+ stored << k
255
+ @stored_attrs << k
256
+ @@redis.set_add("#{self.class.name}:#{id}:attrs", k.to_s)
257
+ @@redis.set("#{self.class.name}:#{@cached_attrs[:id]}:#{k}", @cached_attrs[k])
258
+ end
259
+
260
+ # updated_at
261
+ @@redis.set("#{self.class.name}:#{@cached_attrs[:id]}:updated_at", Time.now.to_f.to_s)
262
+ stored << :updated_at
263
+
264
+ # Relationships
265
+ @@reflections[self.class.name.to_sym][:belongs_to].each do |klass|
266
+ @@redis.set_add("#{klass.to_s.camelize}:#{@cached_attrs[klass.to_s.foreign_key.to_sym]}:_#{self.class.name.underscore}_ids", @cached_attrs[:id])
267
+ end
268
+
269
+ return stored.to_a
270
+ end
271
+
272
+ def destroy
273
+ if @cached_attrs[:id]
274
+ @@redis.list_rm("#{self.class.name}:list", 0, @cached_attrs[:id])
275
+ @cached_attrs.keys.each do |k|
276
+ @@redis.delete("#{self.class.name}:#{@cached_attrs[:id]}:#{k}")
277
+ end
278
+ @@redis.delete("#{self.class.name}:#{id}:attrs")
279
+
280
+ # Reflections
281
+ @@reflections[self.class.name.to_sym][:belongs_to].each do |klass|
282
+ fkey = @cached_attrs["#{klass}_id".to_sym]
283
+ @@redis.set_delete("#{klass.to_s.camelize}:#{fkey}:_#{self.class.name.downcase}_ids", @cached_attrs[:id]) if fkey
284
+ end
285
+
286
+ @cached_attrs = {}
287
+ end
288
+ end
289
+
290
+ # Returns *true* if there are unsaved attributes.
291
+ # p = Post.new(:title => 'Lorem ipsum')
292
+ # p.save
293
+ # p.dirty? # => false
294
+ # p.body = 'Dolor sit amet'
295
+ # p.dirty? # => true
296
+ def dirty?
297
+ (@cached_attrs.keys - @stored_attrs.to_a).empty? ? false : true
298
+ end
299
+
300
+ # Returns *true* if the object wasn't stored before
301
+ def new_record?
302
+ @opts[:stored] == true ? false : true
303
+ end
304
+
305
+ # Current instance attributes
306
+ def attributes
307
+ @cached_attrs.keys
308
+ end
309
+
310
+ # Reload from store, keeps non-stored attributes.
311
+ def reload
312
+ attrs = @@redis.set_members("#{self.class.name}:#{@cached_attrs[:id]}:attrs").map(&:to_sym)
313
+ attrs.each do |attr|
314
+ @cached_attrs[attr] = @@redis.get("#{self.class.name}:#{@cached_attrs[:id]}:#{attr}")
315
+ end
316
+ self
317
+ end
318
+
319
+ # Force reload from store, wipes non-stored attributes.
320
+ def reload!
321
+ @cached_attrs = {:id => @cached_attrs[:id]}
322
+ reload
323
+ end
324
+
325
+ # Set a reverse lookup by attribute.
326
+ def set_lookup(attr)
327
+ self.class._connection.set("#{self.class.name}:lookup:#{attr}:#{self.send(attr)}", self.id) if self.id
328
+ end
329
+
330
+ private
331
+
332
+ # Captures the *instance* missing methods and converts it into instance attributes.
333
+ def method_missing(*args)
334
+ method = args[0].to_s
335
+ case args.length
336
+ when 2
337
+ k, v = method.delete('=').to_sym, args[1]
338
+ add_attribute k, v
339
+ end
340
+ end
341
+
342
+ # Add attributes to the instance cache and define the accessor methods
343
+ def add_attributes(hash)
344
+ hash.each_pair do |k,v|
345
+ k = k.to_sym
346
+ #raise DuplicateAttribute.new("#{k}") unless (k == :id or !self.respond_to?(k))
347
+ if k == :id or !self.respond_to?(k)
348
+ @cached_attrs[k] = v
349
+ meta = class << self; self; end
350
+ meta.send(:define_method, k) { @cached_attrs[k] }
351
+ meta.send(:define_method, "#{k}=") do |new_value|
352
+ @cached_attrs[k] = new_value.is_a?(RedisRecord::Model) ? new_value.id : new_value
353
+ @stored_attrs.delete(k)
354
+ end
355
+ end
356
+ end
357
+ hash
358
+ end
359
+
360
+ # Wraper around add_attributes
361
+ def add_attribute(key, value=nil)
362
+ add_attributes({key => value})
363
+ end
364
+
365
+ # Add the foreign key for the belongs_to relationships
366
+ def add_foreign_keys_as_attributes
367
+ @@reflections[self.class.name.to_sym][:belongs_to].each do |klass|
368
+ add_attribute klass.to_s.foreign_key.to_sym
369
+ end
370
+ end
371
+
372
+ end
373
+
374
+ end
@@ -0,0 +1,112 @@
1
+ # = RedisRecord extensions
2
+ #
3
+ # This extensions should be required manually and add the following specific funcionality:
4
+ #
5
+ # * populate_from_yaml: Allows you to populate a DB from a YAML file.
6
+ # * Sphinx extensions:
7
+ # * sphinx_export: Export records to xmlpipe2-compatible XML files.
8
+ # * search: Sphinx query interface for RedisRecord.
9
+ #
10
+ require 'yaml'
11
+
12
+ begin
13
+ require 'xml'
14
+ rescue LoadError
15
+ $stderr.puts '[error] Missing gem: "libxml-ruby". Try: sudo gem install libxml-ruby'
16
+ end
17
+
18
+ begin
19
+ require 'riddle'
20
+ rescue LoadError
21
+ $stderr.puts '[error] Missing gem: "riddle". Try: sudo gem install riddle'
22
+ end
23
+
24
+ module RedisRecord
25
+ class Base
26
+ class << self
27
+ # IMPORTANT: This method is part of the *RedisRecord extensions*.
28
+ #
29
+ # Accepts a YAML file as input to populate the database.
30
+ #
31
+ # A sample YAML file can be found in the *samples* directory.
32
+ def populate_from_yaml(yaml_file)
33
+ entries = YAML::load_file(yaml_file)
34
+ records = []
35
+ entries.keys.each do |k|
36
+ begin
37
+ r = new(entries[k])
38
+ r.save
39
+ records << r
40
+ rescue Exception => e
41
+ records << nil
42
+ end
43
+ end
44
+ records
45
+ end
46
+
47
+ # IMPORTANT: This method is part of the *RedisRecord extensions*.
48
+ #
49
+ # Generates an Sphinx xmlpipe2-compatible XML file ready to be
50
+ # indexed.
51
+ #
52
+ # Parameters:
53
+ # * records: An array of RedisRecord entries
54
+ # * outfile: Output XML file path. Default is '/tmp/redisindex.xml'
55
+ #
56
+ # Depends on: *libxml-ruby* gem.
57
+ #
58
+ # A sample *sphinx.conf* file can be found in the *samples* directory.
59
+ def sphinx_export(records, outfile='/tmp/redisindex.xml')
60
+ sphinx_docset = XML::Document.new()
61
+ sphinx_docset.root = XML::Node.new('sphinx:docset')
62
+
63
+ sphinx_schema = XML::Node.new('sphinx:schema')
64
+ records_attrs = records.map {|r| r.attrs }
65
+ records_attrs.flatten!.uniq!
66
+ [:created_at, :updated_at].each do |a|
67
+ records_attrs.delete(a)
68
+ sphinx_attr = XML::Node.new('sphinx:attr')
69
+ sphinx_attr.attributes['name'] = a.to_s
70
+ sphinx_attr.attributes['type'] = 'timestamp'
71
+ sphinx_schema << sphinx_attr
72
+ end
73
+
74
+ records_attrs.each do |a|
75
+ sphinx_field = XML::Node.new('sphinx:field')
76
+ sphinx_field.attributes['name']= a.to_s
77
+ sphinx_schema << sphinx_field
78
+ end
79
+ sphinx_attr = XML::Node.new('sphinx:field')
80
+ sphinx_attr.attributes['name'] = 'class'
81
+ sphinx_schema << sphinx_attr
82
+
83
+ sphinx_docset.root << sphinx_schema
84
+
85
+ records.each do |record|
86
+ sphinx_doc = XML::Node.new('sphinx:document')
87
+ sphinx_doc.attributes['id'] = record.id
88
+ sphinx_doc << XML::Node.new('class', record.class.name)
89
+ record.attrs.each {|key| sphinx_doc << XML::Node.new(key, record.send(key).to_s) }
90
+ sphinx_docset.root << sphinx_doc
91
+ end
92
+
93
+ sphinx_docset.save(outfile)
94
+ sphinx_docset
95
+ end
96
+
97
+ # IMPORTANT: This method is part of the *RedisRecord extensions*.
98
+ #
99
+ # Sphinx client for RedisRecord. Accepts Sphinx extended[http://www.sphinxsearch.com/docs/current.html#extended-syntax] query syntax.
100
+ #
101
+ # Depends on: *riddle* gem.
102
+ def search(*args)
103
+ options = args.last.is_a?(Hash) ? args.pop : {}
104
+ client = Riddle::Client.new
105
+ client.match_mode = :extended
106
+ q = client.query("#{args} @class #{name}")
107
+ ids = q[:matches].map {|r| r[:doc]}
108
+ find(ids)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,66 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "RedisRecord" do
4
+
5
+ before(:each) do
6
+ @c = Customer.new
7
+ end
8
+
9
+ after do
10
+ r = Redis.new
11
+ #r.select_db 15
12
+ r.flush_db
13
+ end
14
+
15
+ it "should allow to add any attribute to an instance" do
16
+ @c.name = 'foo'
17
+ @c.age = 25
18
+ @c.name.should == 'foo'
19
+ @c.age.should == 25
20
+ end
21
+
22
+ it "should have an attribute accesor with the instance attributes" do
23
+ @c.name = 'foo'
24
+ @c.age = 25
25
+ @c.attrs.map {|a| a.to_s }.sort.should == ['age', 'name']
26
+ end
27
+
28
+ it "should be 'dirty' if some of the instance attributes aren't saved" do
29
+ @c.name = 'foo'
30
+ @c.dirty?.should == true
31
+ end
32
+
33
+ it "should not be 'dirty' anymore once saved" do
34
+ @c.name = 'foo'
35
+ @c.save
36
+ @c.dirty?.should == false
37
+ end
38
+
39
+ it "should be 'dirty' again after add a new attribute or update an existing one" do
40
+ @c.name = 'foo'
41
+ @c.save
42
+ @c.age = 25
43
+ @c.dirty?.should == true
44
+ @c.save
45
+ @c.name = 'bar'
46
+ @c.dirty?.should == true
47
+ end
48
+
49
+ it "should have :id and timestamp attributes after saved" do
50
+ @c.name = 'foo'
51
+ saved_attributes = @c.save
52
+ saved_attributes.map {|a| a.to_s }.sort.should == ['created_at','id','name','updated_at']
53
+ end
54
+
55
+ it "should save only: the new and the updated attributes, and update the :updated_at timestamp" do
56
+ @c.name = 'foo'
57
+ @c.lastname = 'bar'
58
+ saved_attributes = @c.save
59
+ saved_attributes.map {|a| a.to_s }.sort.should == ['created_at','id','lastname','name','updated_at']
60
+ @c.age = 25
61
+ @c.lastname = 'baz'
62
+ new_saved_attributes = @c.save
63
+ new_saved_attributes.map {|a| a.to_s }.sort.should == ['age','lastname','updated_at']
64
+ end
65
+
66
+ end
@@ -0,0 +1,82 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "RedisRecord" do
4
+
5
+ before do
6
+ @c1 = Customer.new
7
+ @c1.name = 'Foo'
8
+ @c1.age = 25
9
+ @c1.save
10
+
11
+ @c2 = Customer.new
12
+ @c2.name = 'Bar'
13
+ @c2.age = 30
14
+ @c2.save
15
+
16
+ @c3 = Customer.new
17
+ @c3.name = 'Baz'
18
+ @c3.age = 35
19
+ @c3.save
20
+ end
21
+
22
+ after do
23
+ r = Redis.new
24
+ #r.select_db 15
25
+ r.flush_db
26
+ end
27
+
28
+ it "should find a stored Customer by id" do
29
+ customer = Customer.find(@c1.id)
30
+ customer.attrs.map {|a| a.to_s }.sort.should == ['age','created_at','id','name','updated_at']
31
+ customer.new_record?.should == false
32
+ customer.dirty?.should == false
33
+ end
34
+
35
+ it "should find an array of Customers by id" do
36
+ customers = Customer.find(@c1.id, @c2.id)
37
+ customers.length.should == 2
38
+ customers.each do |c|
39
+ c.attrs.map {|a| a.to_s }.sort.should == ['age','created_at','id','name','updated_at']
40
+ c.new_record?.should == false
41
+ c.dirty?.should == false
42
+ ['Foo', 'Bar'].include?(c.name).should == true
43
+ end
44
+ end
45
+
46
+ it "should find the :first Customer" do
47
+ customer = Customer.find(:first)
48
+ customer.name.should == 'Foo'
49
+ end
50
+
51
+ it "should find the :last Customer" do
52
+ customer = Customer.find(:last)
53
+ customer.name.should == 'Baz'
54
+ end
55
+
56
+ it "should find :all the Customers" do
57
+ customers = Customer.find(:all)
58
+ customers.each do |c|
59
+ c.attrs.map {|a| a.to_s }.sort.should == ['age','created_at','id','name','updated_at']
60
+ c.new_record?.should == false
61
+ c.dirty?.should == false
62
+ [@c1.id, @c2.id, @c3.id].map {|id| id.to_s}.include?(c.id).should == true
63
+ end
64
+ end
65
+
66
+ it "should return the last 10 or less Customers in descendent order of creation when sorting by :created_at" do
67
+ customers = Customer.sort_by(:created_at)
68
+ names_by_created_at = ['Baz', 'Bar', 'Foo']
69
+ until customers.empty?
70
+ customers.pop.name.should == names_by_created_at.pop
71
+ end
72
+ end
73
+
74
+ it "should return the first 2 Customers in ascendent order when sorting by :age" do
75
+ customers = Customer.sort_by(:age, :limit => 2, :order => 'ALPHA ASC')
76
+ ordered_ages = ['25','30']
77
+ until customers.empty?
78
+ customers.pop.age.should == ordered_ages.pop
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "RedisRecord" do
4
+
5
+ before do
6
+ # User
7
+ @u1 = User.new(:name => 'Foo')
8
+ @u1.save
9
+ #puts "User1 => #{@u1.inspect}"
10
+
11
+ # Posts
12
+ @p1 = Post.new(:title => 'Post1', :body => 'Lorem ipsum')
13
+ @p1.user_id = @u1.id
14
+ @p1.save
15
+ #puts "Post1 => #{@p1.inspect}"
16
+ @p2 = Post.new(:title => 'Post2', :body => 'Lorem ipsum')
17
+ @p2.user_id = @u1.id
18
+ @p2.save
19
+ #puts "Post2 => #{@p2.inspect}"
20
+
21
+ # Comments
22
+ @c1 = Comment.new(:text => 'Comment1|Post1')
23
+ @c1.user_id = @u1.id
24
+ @c1.post_id = @p1.id
25
+ @c1.save
26
+ #puts "Comment1 => #{@c1.inspect}"
27
+ @c2 = Comment.new(:text => 'Comment2|Post1')
28
+ @c2.user_id = @u1.id
29
+ @c2.post_id = @p1.id
30
+ @c2.save
31
+ #puts "Comment2 => #{@c2.inspect}"
32
+ end
33
+
34
+ after do
35
+ r = Redis.new
36
+ #r.select_db 15
37
+ r.flush_db
38
+ end
39
+
40
+ it "should find the Posts/Comments relationships through the :has_many methods of the User" do
41
+ posts = @u1.posts
42
+ posts.length.should == 2
43
+ posts.each do |p|
44
+ ['Post1','Post2'].include?(p.title).should == true
45
+ end
46
+ comments = @u1.comments
47
+ comments.length.should == 2
48
+ comments.each do |c|
49
+ ['Comment1|Post1','Comment2|Post1'].include?(c.text).should == true
50
+ end
51
+ end
52
+
53
+ it "should find the User relationship through the Post :belongs_to method" do
54
+ @p1.user.id.should == '1'
55
+ end
56
+
57
+ it "should find the User/Post relationships through the Comment :belongs_to methods" do
58
+ @c1.user.id.should == '1'
59
+ @c1.post.id.should == '1'
60
+ end
61
+
62
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ $TESTING=true
3
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'redisrecord'
5
+
6
+ # Spec helper
7
+ class Customer < RedisRecord::Model
8
+ #database 15
9
+ end
10
+
11
+ # Spec helper
12
+ class User < RedisRecord::Model
13
+ #database 15
14
+ has_many :posts
15
+ has_many :comments
16
+ end
17
+
18
+ # Spec helper
19
+ class Post < RedisRecord::Model
20
+ #database 15
21
+ belongs_to :user
22
+ has_many :comments
23
+ end
24
+
25
+ # Spec helper
26
+ class Comment < RedisRecord::Model
27
+ #database 15
28
+ belongs_to :post
29
+ belongs_to :user
30
+ end
31
+
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redisrecord
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Mauro Pompilio
8
+ autorequire: redisrecord
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-25 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: A 'virtual' ORM on top of Redis.
26
+ email: hackers.are.rockstars@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ files:
34
+ - LICENSE
35
+ - README.markdown
36
+ - lib/redisrecord.rb
37
+ - lib/redisrecord/extensions.rb
38
+ - spec/spec_helper.rb
39
+ - spec/relationships_spec.rb
40
+ - spec/basic_spec.rb
41
+ - spec/finder_spec.rb
42
+ - examples/demo.rb
43
+ - examples/sphinx.conf
44
+ - examples/users.yml
45
+ has_rdoc: true
46
+ homepage: http://github.com/malditogeek/redisrecord
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.5
70
+ signing_key:
71
+ specification_version: 2
72
+ summary: A 'virtual' ORM on top of Redis.
73
+ test_files: []
74
+