redisrecord 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+