redis-orm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +4 -0
  3. data/README.md +189 -0
  4. data/Rakefile +1 -0
  5. data/lib/redis-orm.rb +32 -0
  6. data/lib/redis/actions.rb +21 -0
  7. data/lib/redis/actions/creating.rb +31 -0
  8. data/lib/redis/actions/destroying.rb +39 -0
  9. data/lib/redis/actions/finding.rb +26 -0
  10. data/lib/redis/actions/saving.rb +47 -0
  11. data/lib/redis/actions/timestamps.rb +19 -0
  12. data/lib/redis/actions/updating.rb +18 -0
  13. data/lib/redis/orm.rb +59 -0
  14. data/lib/redis/orm/attributes.rb +65 -0
  15. data/lib/redis/orm/version.rb +14 -0
  16. data/lib/redis/relations.rb +22 -0
  17. data/lib/redis/relations/belongs_to.rb +51 -0
  18. data/lib/redis/relations/has_many.rb +60 -0
  19. data/lib/redis/relations/has_one.rb +51 -0
  20. data/lib/redis/validations.rb +9 -0
  21. data/lib/redis/validations/uniqueness.rb +41 -0
  22. data/redis-orm.gemspec +25 -0
  23. data/spec/redis/actions/creating_spec.rb +32 -0
  24. data/spec/redis/actions/destroying_spec.rb +27 -0
  25. data/spec/redis/actions/finding_spec.rb +26 -0
  26. data/spec/redis/actions/timestamps_spec.rb +11 -0
  27. data/spec/redis/actions/updating_spec.rb +32 -0
  28. data/spec/redis/orm_spec.rb +56 -0
  29. data/spec/redis/relations/belongs_to_spec.rb +54 -0
  30. data/spec/redis/relations/has_many_spec.rb +33 -0
  31. data/spec/redis/relations/has_one_spec.rb +23 -0
  32. data/spec/redis/relations/interop_spec.rb +45 -0
  33. data/spec/redis/validations/uniqueness_spec.rb +32 -0
  34. data/spec/spec_helper.rb +12 -0
  35. data/spec/support/active_model_lint.rb +20 -0
  36. data/spec/support/redis_helper.rb +27 -0
  37. data/test.rb +23 -0
  38. metadata +136 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in redis-orm.gemspec
4
+ gemspec
@@ -0,0 +1,189 @@
1
+ # redis-orm
2
+
3
+ Easy-to-use object relational mapping for Redis.
4
+
5
+ ## Disclaimer
6
+
7
+ The gem is intended to be similar to ActiveRecord; however, it's not identical. Most notably, a lot of options that ActiveRecord gives you are currently missing from `redis-orm`. This is mostly because I'm just one guy, and I have other projects I'd like to make some headway on. As time goes on, expect `redis-orm` to become more and more ActiveRecord-like. The end goal is to produce a Redis library that is a drop-in replacement for ActiveRecord in a Rails 3 project (but that is also usable outside of Rails).
8
+
9
+ And by the way, I'm totally accepting pull requests. Hint, hint.
10
+
11
+ ## Requiring
12
+
13
+ Add it to your Gemfile if you're using one. If necessary, require the redis-orm libraries like so:
14
+
15
+ require 'redis-orm'
16
+
17
+ # -or-
18
+
19
+ require 'redis/orm'
20
+
21
+ ...whichever one strikes your fancy.
22
+
23
+ ## Usage
24
+
25
+ Inherit from `Redis::ORM` as if you were inheriting from `ActiveRecord::Base`:
26
+
27
+ class User < Redis::ORM
28
+ # more code here!
29
+ end
30
+
31
+ At this point your Accounts can be created, saved, and so forth. It might be helpful to define attributes on the model, however, or else the model will be quite useless indeed!
32
+
33
+ ### attributes
34
+
35
+ Instead of defining them in a schema file or set of migrations, `redis-orm` defines attributes directly within the model:
36
+
37
+ class User < Redis::ORM
38
+ attribute :login
39
+ attribute :password
40
+ end
41
+
42
+ Attributes can have any value that can be serialized. The default serializer is `Marshal`, but more on that later.
43
+
44
+ Optionally, you can give attributes a default value. If not specified, the default value is implicitly `nil`.
45
+
46
+ class Post < Redis::ORM
47
+ attribute :subject, "(No subject.)"
48
+ attribute :body
49
+ end
50
+
51
+ Like `ActiveRecord`, `redis-orm` defines `created_at` and `updated_at` attributes for you automatically, so you don't need to worry about these. Additionally, the `id` field is created and managed for you.
52
+
53
+ ### relationships
54
+
55
+ There are 3 core relationships defined by `redis-orm`: `belongs_to`, `has_one` and `has_many`. They function essentially similar to relations of the same name in ActiveRecord, but it's important to keep in mind that they are specifically designed for Redis, and they have some minor differences.
56
+
57
+ class User < Redis::ORM
58
+ attribute :login
59
+ attribute :password
60
+ has_many :posts
61
+ has_one :profile
62
+ end
63
+
64
+ class Post < Redis::ORM
65
+ attribute :subject, "(no subject)"
66
+ attribute :body
67
+ belongs_to :user
68
+ end
69
+
70
+ class Profile < Redis::ORM
71
+ belongs_to :user
72
+ end
73
+
74
+ #### inference
75
+
76
+ Unlike `ActiveRecord`, `redis-orm` does _not_ infer class names from the relation name. You can give any value you like to the relations. During look-up, class names are retrieved from the object's ID, which (as mentioned) is already maintained for you. So in most cases, you should not have to care about the object's class at all. The only caveat is, all related objects _must_ inherit from `Redis::ORM` so that they can be looked up and deserialized properly.
77
+
78
+ ### validations
79
+
80
+ The `Redis::ORM` base class automatically pulls in `ActiveModel::Validations`, so you should look at those. In addition, a Redis-specific `validates_uniqueness_of` validation has been added, and can be used thusly:
81
+
82
+ class User < Redis::ORM
83
+ attribute :login
84
+
85
+ validates_uniqueness_of :login
86
+ end
87
+
88
+ ## Actions
89
+
90
+ Now we get to discuss things you can actually _do_ with your Redis-based models!
91
+
92
+ ### creating and saving records
93
+
94
+ You can instantiate a new record by simply calling _new_, just like any Ruby object. Supply an optional hash of attributes:
95
+
96
+ new_user = User.new() #=> a user with no attributes
97
+ new_user = User.new(:login => "Colin") #=> a user named Colin, but with no password
98
+ new_user = User.new(:profile => Profile.new) #=> a user with a profile (it has_one, remember?)
99
+
100
+ So far, none of these records have been saved. Let's do that now:
101
+
102
+ if new_user.save
103
+ # save successful, do something
104
+ else
105
+ # save failed, let's get the error messages
106
+ puts new_user.errors.full_messages
107
+ end
108
+
109
+ The `save` method returns `true` if the save was successful, `false` otherwise. If the save failed, the model's `errors` object will have been populated.
110
+
111
+ The `errors` object comes straight from `ActiveModel`, so using it is identical to that of `ActiveRecord`.
112
+
113
+ You can call `save!` instead of `save` if you prefer an actual exception to be raised if the record couldn't be saved. The exception will include the full error messages, so you will know if the save failed due to validations.
114
+
115
+ You can also make use of the ActiveRecord-esque `create` method:
116
+
117
+ new_user = User.create(:login => 'Colin')
118
+
119
+ The model instance will be returned whether the record was successfully saved or not. If you would prefer an error to be raised upon failing validations, you can call `create!` instead.
120
+
121
+ ### finding records
122
+
123
+ Finding an existing record is done like so:
124
+
125
+ user = User.find('id_of_user')
126
+
127
+ If the record could not be found, `nil` will be returned.
128
+
129
+ Dynamic `find_by_*` methods are not supported at this time.
130
+
131
+ ### destroying records
132
+
133
+ Destroying records is easy:
134
+
135
+ user = User.find('id_of_user')
136
+ user.destroy
137
+
138
+ To destroy all records of a given type, call `destroy_all` on the model class:
139
+
140
+ User.destroy_all
141
+
142
+ ## Callbacks
143
+
144
+ The following callbacks are supported, and are used just like their `ActiveRecord` counterparts:
145
+
146
+ * before_initialize
147
+ * after_initialize
148
+ * around_initialize
149
+ * before_save
150
+ * after_save
151
+ * around_save
152
+ * before_create
153
+ * after_create
154
+ * around_create
155
+ * before_update
156
+ * after_update
157
+ * around_update
158
+ * before_destroy
159
+ * after_destroy
160
+ * around_destroy
161
+
162
+ In addition, you can register behavior to happen _during_ some actions, specifically saving and destroying:
163
+
164
+ class User < Redis::ORM
165
+ within_save_block :save_extra_stuff
166
+ within_destroy_block :delete_extra_stuff
167
+
168
+ def save_extra_stuff
169
+ # hardcore low-level database action
170
+ end
171
+
172
+ def delete_extra_stuff
173
+ # crazy low-level database madness
174
+ end
175
+ end
176
+
177
+ You can use these callbacks to perform commands directly upon the Redis database connection, and they will be rolled into the corresponding save and destroy transaction blocks. This way, if the saving or destroying of the model should fail at any point during the save or destroy process respectively, the entire transaction will be rolled back and you can rest assured that you haven't needlessly altered the database beyond recognition.
178
+
179
+ ## Configuration
180
+
181
+ You can set the host and port for Redis:
182
+
183
+ Redis.host = 'localhost'
184
+ Redis.port = 3200
185
+
186
+ If you already have an active connection, however, these changes will not take effect. You'll need to replace the connection directly:
187
+
188
+ Redis.connection = Redis.new(:host => 'localhost', :port => 3000)
189
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,32 @@
1
+ require 'redis'
2
+
3
+ class Redis
4
+ autoload :ORM, 'redis/orm'
5
+
6
+ @@connection = nil
7
+ class << self
8
+ def connection
9
+ @@connection ||= Redis.new(:host => host, :port => port)
10
+ end
11
+
12
+ def connection=(redis)
13
+ @@connection = redis
14
+ end
15
+
16
+ def host
17
+ @host ||= 'localhost'
18
+ end
19
+
20
+ def port
21
+ @port ||= 6379
22
+ end
23
+
24
+ def host=(a)
25
+ @host = a
26
+ end
27
+
28
+ def port=(a)
29
+ @port = a
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ class Redis
2
+ module Actions
3
+ autoload :Finding, "redis/actions/finding"
4
+ autoload :Destroying, "redis/actions/destroying"
5
+ autoload :Saving, "redis/actions/saving"
6
+ autoload :Updating, "redis/actions/updating"
7
+ autoload :Creating, "redis/actions/creating"
8
+ autoload :Timestamps, "redis/actions/timestamps"
9
+
10
+ def self.included(base)
11
+ base.class_eval do
12
+ include Redis::Actions::Finding
13
+ include Redis::Actions::Destroying
14
+ include Redis::Actions::Saving
15
+ include Redis::Actions::Updating
16
+ include Redis::Actions::Creating
17
+ include Redis::Actions::Timestamps
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ module Redis::Actions::Creating
2
+ def self.included(base)
3
+ base.send :extend, Redis::Actions::Creating::ClassMethods
4
+ base.define_model_callbacks :create
5
+ base.around_save :create_if_new
6
+ end
7
+
8
+ def create_if_new
9
+ if new_record?
10
+ run_callbacks(:create) do
11
+ yield
12
+ end
13
+ else
14
+ yield
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def create(attributes = {})
20
+ record = new(attributes)
21
+ record.save
22
+ record
23
+ end
24
+
25
+ def create!(attributes = {})
26
+ record = new(attributes)
27
+ record.save!
28
+ record
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ module Redis::Actions::Destroying
2
+ def self.included(base)
3
+ base.class_eval do
4
+ extend Redis::Actions::Destroying::ClassMethods
5
+ define_model_callbacks :destroy
6
+ class_inheritable_array :within_destroy_blocks
7
+ self.within_destroy_blocks ||= []
8
+ end
9
+ end
10
+
11
+ def destroy
12
+ unless new_record?
13
+ # run_callbacks(:before_destroy)
14
+ run_callbacks(:destroy) do
15
+ transaction do
16
+ connection.del key
17
+ within_destroy_blocks.each do |method_name_or_block|
18
+ if method_name_or_block.kind_of?(String) || method_name_or_block.kind_of?(Symbol)
19
+ send method_name_or_block
20
+ else
21
+ method_name_or_block.call self
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ def destroy_all
31
+ all.each { |orm| orm.destroy }
32
+ end
33
+
34
+ def within_destroy_block(method_name = nil, &block)
35
+ within_destroy_blocks << method_name if method_name
36
+ within_destroy_blocks << block if block_given?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ module Redis::Actions::Finding
2
+ def self.included(base)
3
+ base.send :extend, Redis::ORM::Finding::ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def find(key)
8
+ data = connection.get(key)
9
+ if data
10
+ klass_name = key.split(/\//)[0]
11
+ klass = (klass_name.camelize.constantize rescue self)
12
+ instance = klass.new(serializer.load(data))
13
+ instance.set_unchanged!
14
+ instance
15
+ else
16
+ nil
17
+ end
18
+ end
19
+
20
+ def all
21
+ connection.hgetall(File.join(model_name, "keys")).collect do |key|
22
+ find(key.first)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ module Redis::Actions::Saving
2
+ def self.included(base)
3
+ base.class_eval do
4
+ define_model_callbacks :save
5
+ class_inheritable_array :within_save_blocks
6
+ self.within_save_blocks ||= []
7
+ extend Redis::Actions::Saving::ClassMethods
8
+ end
9
+ end
10
+
11
+ def save
12
+ if valid?
13
+ define_key if key.nil?
14
+ run_callbacks(:save) do
15
+ transaction do
16
+ within_save_blocks.each do |block_or_method|
17
+ if block_or_method.kind_of?(String) || block_or_method.kind_of?(Symbol)
18
+ send block_or_method
19
+ else
20
+ block_or_method.call self
21
+ end
22
+ end
23
+ connection.set(key, serializer.dump(attributes))
24
+ end
25
+ end
26
+ set_unchanged!
27
+ true
28
+ else
29
+ false
30
+ end
31
+ end
32
+
33
+ def save!
34
+ raise "Record was not saved: #{errors.full_messages}" unless save
35
+ end
36
+
37
+ def define_key
38
+ self.key = File.join(model_name, connection.incr("__uniq__").to_s)
39
+ end
40
+
41
+ module ClassMethods
42
+ def within_save_block(method_name = nil, &block)
43
+ within_save_blocks << method_name if method_name
44
+ within_save_blocks << block if block_given?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ module Redis::Actions::Timestamps
2
+ def updated_at_timestamp
3
+ self.updated_at = Time.now
4
+ end
5
+
6
+ def created_at_timestamp
7
+ self.created_at ||= Time.now
8
+ end
9
+
10
+ def self.included(base)
11
+ base.instance_eval do
12
+ attribute :created_at
13
+ attribute :updated_at
14
+
15
+ before_save :updated_at_timestamp
16
+ before_create :created_at_timestamp
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Redis::Actions::Updating
2
+ def self.included(base)
3
+ base.class_eval do
4
+ base.define_model_callbacks :update
5
+ around_save :update_if_not_new
6
+ end
7
+ end
8
+
9
+ def update_if_not_new
10
+ if new_record?
11
+ yield
12
+ else
13
+ run_callbacks(:update) do
14
+ yield
15
+ end
16
+ end
17
+ end
18
+ end