ohm 0.1.0.rc6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -6,10 +6,9 @@ Object-hash mapping library for Redis.
6
6
  Description
7
7
  -----------
8
8
 
9
- Ohm is a library for storing objects in
10
- [Redis](http://code.google.com/p/redis/), a persistent key-value
11
- database. It includes an extensible list of validations and has very
12
- good performance.
9
+ Ohm is a library for storing objects in [Redis][redis], a persistent key-value
10
+ database. It includes an extensible list of validations and has very good
11
+ performance.
13
12
 
14
13
  Community
15
14
  ---------
@@ -21,9 +20,8 @@ Meet us on IRC: [#ohm](irc://chat.freenode.net/#ohm) on [freenode.net](http://fr
21
20
  Getting started
22
21
  ---------------
23
22
 
24
- Install [Redis](http://code.google.com/p/redis/). On most platforms
25
- it's as easy as grabbing the sources, running make and then putting the
26
- `redis-server` binary in the PATH.
23
+ Install [Redis][redis]. On most platforms it's as easy as grabbing the sources,
24
+ running make and then putting the `redis-server` binary in the PATH.
27
25
 
28
26
  Once you have it installed, you can execute `redis-server` and it will
29
27
  run on `localhost:6379` by default. Check the `redis.conf` file that comes
@@ -31,9 +29,9 @@ with the sources if you want to change some settings.
31
29
 
32
30
  If you don't have Ohm, try this:
33
31
 
34
- $ sudo gem install ohm
32
+ $ [sudo] gem install ohm
35
33
 
36
- Or you can grab the code from [http://github.com/soveran/ohm](http://github.com/soveran/ohm).
34
+ Or you can grab the code from [http://github.com/soveran/ohm][ohm].
37
35
 
38
36
  Now, in an irb session you can test the Redis adapter directly:
39
37
 
@@ -46,6 +44,48 @@ Now, in an irb session you can test the Redis adapter directly:
46
44
  >> Ohm.redis.get "Foo"
47
45
  => "Bar"
48
46
 
47
+ ## Connecting to the Redis database {: #connecting }
48
+
49
+ There are a couple of different strategies for connecting to your Redis
50
+ database. The first is to explicitly set the `:host`, `:port`, `:db` and
51
+ `:timeout` options. You can also set only a few of them, and let the other
52
+ options fall back to the default.
53
+
54
+ The other noteworthy style of connecting is by just doing `Ohm.connect` and
55
+ set the environment variable `REDIS_URL`.
56
+
57
+ Here are the options for {Ohm.connect} in detail:
58
+
59
+ **:url**
60
+ : A Redis URL of the form `redis://:<passwd>@<host>:<port>/<db>`.
61
+ Note that if you specify a URL and one of the other options at
62
+ the same time, the other options will take precedence. Also, if
63
+ you try and do `Ohm.connect` without any arguments, it will check
64
+ if `ENV["REDIS_URL"]` is set, and will use it as the argument for
65
+ `:url`.
66
+
67
+ **:host**
68
+ : Host where the Redis server is running, defaults to `"127.0.0.1"`.
69
+
70
+ **:port**
71
+ : Port number, defaults to `6379`.
72
+
73
+ **:db**
74
+ : Database number, defaults to `0`.
75
+
76
+ **:password**
77
+ : It is the secret that will be sent to the Redis server. Use it if the server
78
+ configuration requires it. Defaults to `nil`.
79
+
80
+ **:timeout**
81
+ : Database timeout in seconds, defaults to `0`.
82
+
83
+ **:thread_safe**
84
+ : Initializes the client with a monitor. It has a small performance penalty, and
85
+ it's off by default. For thread safety, it is recommended to use a different
86
+ instance per thread. I you have no choice, then pass `:thread_safe => true`
87
+ when connecting.
88
+
49
89
  Models
50
90
  ------
51
91
 
@@ -99,10 +139,10 @@ validations. Keep reading to find out what you can do with models.
99
139
  Attribute types
100
140
  ---------------
101
141
 
102
- Ohm::Model provides four attribute types: {Ohm::Model::attribute
103
- attribute}, {Ohm::Model::set set}, {Ohm::Model::list list}
104
- and {Ohm::Model::counter counter}; and two meta types:
105
- {Ohm::Model::reference reference} and {Ohm::Model::collection
142
+ Ohm::Model provides four attribute types: {Ohm::Model.attribute
143
+ attribute}, {Ohm::Model.set set}, {Ohm::Model.list list}
144
+ and {Ohm::Model.counter counter}; and two meta types:
145
+ {Ohm::Model.reference reference} and {Ohm::Model.collection
106
146
  collection}.
107
147
 
108
148
  ### attribute
@@ -162,13 +202,13 @@ If you need to check for validity before operating on lists, sets or
162
202
  counters, you can use this pattern:
163
203
 
164
204
  if event.valid?
165
- event.comments << "Great event!"
205
+ event.comments << Comment.create(:body => "Great event!")
166
206
  end
167
207
 
168
208
  If you are saving the object, this will suffice:
169
209
 
170
210
  if event.save
171
- event.comments << "Wonderful event!"
211
+ event.comments << Comment.create(:body => "Wonderful event!")
172
212
  end
173
213
 
174
214
  Working with Sets
@@ -184,22 +224,84 @@ Given the following model declaration:
184
224
  You can add instances of `Person` to the set of attendees with the
185
225
  `<<` method:
186
226
 
187
- @event.attendees << Person.create(name: "Albert")
227
+ event.attendees << Person.create(:name => "Albert")
188
228
 
189
229
  # And now...
190
- @event.attendees.each do |person|
230
+ event.attendees.each do |person|
191
231
  # ...do what you want with this person.
192
232
  end
193
233
 
194
- Sorting
195
- -------
234
+ ## Sorting {: #sorting}
196
235
 
197
236
  Since `attendees` is a {Ohm::Model::Set Set}, it exposes two sorting
198
237
  methods: {Ohm::Model::Collection#sort sort} returns the elements
199
238
  ordered by `id`, and {Ohm::Model::Collection#sort_by sort_by} receives
200
239
  a parameter with an attribute name, which will determine the sorting
201
- order. Both methods receive an options hash which is explained in the
202
- documentation for {Ohm::Model::Collection#sort sort}.
240
+ order. Both methods receive an options hash which is explained below:
241
+
242
+ **:order**
243
+ : Order direction and strategy. You can pass in any of
244
+ the following:
245
+
246
+ 1. ASC
247
+ 2. ASC ALPHA (or ALPHA ASC)
248
+ 3. DESC
249
+ 4. DESC ALPHA (or ALPHA DESC)
250
+
251
+ It defaults to `ASC`.
252
+
253
+ **:start**
254
+ : The offset from which we should start with. Note that
255
+ this is 0-indexed. It defaults to `0`.
256
+
257
+ **:limit**
258
+ : The number of entries to get. If you don't pass in anything, it will
259
+ get all the results from the LIST or SET that you are sorting.
260
+
261
+ **:by**
262
+ : Key or Hash key with which to sort by. An important distinction with
263
+ using {Ohm::Model::Collection#sort sort} and
264
+ {Ohm::Model::Collection#sort_by sort_by} is that `sort_by` automatically
265
+ converts the passed argument with the assumption that it is a hash key
266
+ and it's within the current model you are sorting.
267
+
268
+ Post.all.sort_by(:title) # SORT Post:all BY Post:*->title
269
+ Post.all.sort(:by => :title) # SORT Post:all BY title
270
+
271
+ **:get**
272
+ : A key pattern to return, e.g. `Post:*->title`. As is the case with
273
+ the `:by` option, using {Ohm::Model::Collection#sort sort} and
274
+ {Ohm::Model::Collection#sort_by sort_by} has distinct differences in
275
+ that `sort_by` does much of the hand-coding for you.
276
+
277
+ Post.all.sort_by(:title, :get => :title)
278
+ # SORT Post:all BY Post:*->title GET Post:*->title
279
+
280
+ Post.all.sort(:by => :title, :get => :title)
281
+ # SORT Post:all BY title GET title
282
+
283
+
284
+ **:store**
285
+ : An optional key which you may use to cache the sorted result. The key
286
+ may or may not exist.
287
+
288
+ This option can only be used together with `:get`.
289
+
290
+ The type that is used for the STORE key is a LIST.
291
+
292
+ Post.all.sort_by(:title, :store => "FOO")
293
+
294
+ # Get all the results stored in FOO.
295
+ Post.db.lrange("FOO", 0, -1)
296
+
297
+ When using temporary values, it might be a good idea to use a `volatile`
298
+ key. In Ohm, a volatile key means it just starts with a `~` character.
299
+
300
+ Post.all.sort_by(:title, :get => :title,
301
+ :store => Post.key.volatile["FOO"])
302
+
303
+ Post.key.volatile["FOO"].lrange 0, -1
304
+
203
305
 
204
306
  Associations
205
307
  ------------
@@ -219,19 +321,85 @@ Ohm lets you declare `references` and `collections` to represent associations.
219
321
 
220
322
  After this, every time you refer to `post.comments` you will be talking
221
323
  about instances of the model `Comment`. If you want to get a list of IDs
222
- you can use `post.comments.raw`.
324
+ you can use `post.comments.key.smembers`.
325
+
326
+ ### References explained {: #references }
327
+
328
+ Doing a {Ohm::Model.reference reference} is actually just a shortcut for
329
+ the following:
330
+
331
+ # Redefining our model above
332
+ class Comment < Ohm::Model
333
+ attribute :body
334
+ attribute :post_id
335
+ index :post_id
336
+
337
+ def post=(post)
338
+ self.post_id = post.id
339
+ end
340
+
341
+ def post
342
+ Post[post_id]
343
+ end
344
+ end
345
+
346
+ _(The only difference with the actual implementation is that the model
347
+ is memoized.)_
348
+
349
+ The net effect here is we can conveniently set and retrieve `Post` objects,
350
+ and also search comments using the `post_id` index.
351
+
352
+ Comment.find(:post_id => 1)
353
+
354
+
355
+ ### Collections explained {: #collections }
356
+
357
+ The reason a {Ohm::Model.reference reference} and a
358
+ {Ohm::Model.collection collection} go hand in hand, is that a collection is
359
+ just a macro that defines a finder for you, and we know that to find a model
360
+ by a field requires an {Ohm::Model.index index} to be defined for the field
361
+ you want to search.
362
+
363
+ # Redefining our post above
364
+ class Post < Ohm::Model
365
+ attribute :title
366
+ attribute :body
367
+
368
+ def comments
369
+ Comment.find(:post_id => self.id)
370
+ end
371
+ end
372
+
373
+ The only "magic" happening is with the inference of the `index` that was used
374
+ in the other model. The following all produce the same effect:
375
+
376
+ # easiest, with the basic assumption that the index is `:post_id`
377
+ collection :comments, Comment
378
+
379
+ # we can explicitly declare this as follows too:
380
+ collection :comments, Comment, :post
381
+
382
+ # finally, we can use the default argument for the third parameter which
383
+ # is `to_reference`.
384
+ collection :comments, Comment, to_reference
385
+
386
+ # exploring `to_reference` reveals a very interesting and simple concept:
387
+ Post.to_reference == :post
388
+ # => true
223
389
 
224
390
  Indexes
225
391
  -------
226
392
 
227
- An index is a set that's handled automatically by Ohm. For any index declared,
228
- Ohm maintains different sets of objects IDs for quick lookups.
393
+ An {Ohm::Model.index index} is a set that's handled automatically by Ohm. For
394
+ any index declared, Ohm maintains different sets of objects IDs for quick
395
+ lookups.
229
396
 
230
397
  In the `Event` example, the index on the name attribute will
231
398
  allow for searches like `Event.find(:name => "some value")`.
232
399
 
233
- Note that the `assert_unique` validation and the methods `find` and `except` need a
234
- corresponding index in order to work.
400
+ Note that the {Ohm::Model::Validations#assert_unique assert_unique}
401
+ validation and the methods {Ohm::Model::Set#find find} and
402
+ {Ohm::Model::Set#except except} need a corresponding index in order to work.
235
403
 
236
404
  ### Finding records
237
405
 
@@ -372,6 +540,20 @@ Ohm is rather small and can be extended in many ways.
372
540
 
373
541
  A lot of amazing contributions are available at [Ohm Contrib](http://labs.sinefunc.com/ohm-contrib/doc/), make sure to check them if you need to extend Ohm's functionality.
374
542
 
543
+ Tutorials
544
+ =========
545
+
546
+ Check the examples to get a feeling of the design patterns for Redis.
547
+
548
+ 1. [Activity Feed](examples/activity-feed.html)
549
+ 2. [Chaining finds](examples/chaining.html)
550
+ 3. [Serialization to JSON](examples/json-hash.html)
551
+ 4. [One to many associations](examples/one-to-many.html)
552
+ 5. [Philosophy behind Ohm](examples/philosophy.html)
553
+ 6. [Learning Ohm internals](examples/redis-logging.html)
554
+ 7. [Slugs and permalinks](examples/slug.html)
555
+ 8. [Tagging](examples/tagging.html)
556
+
375
557
  Versions
376
558
  ========
377
559
 
@@ -396,3 +578,7 @@ don't have to load your application environment. Since we assume it's
396
578
  very likely that you have a bunch of data, the script uses
397
579
  [Batch](http://github.com/djanowski/batch) to show you some progress
398
580
  while the process runs.
581
+
582
+
583
+ [redis]: http://redis.io
584
+ [ohm]: http://github.com/soveran/ohm
data/Rakefile CHANGED
@@ -20,6 +20,7 @@ desc "Stop the Redis server"
20
20
  task :stop do
21
21
  if File.exists?(REDIS_PID)
22
22
  system "kill #{File.read(REDIS_PID)}"
23
+ File.delete(REDIS_PID)
23
24
  end
24
25
  end
25
26
 
@@ -28,3 +29,17 @@ task :test do
28
29
 
29
30
  Cutest.run(Dir["test/*_test.rb"])
30
31
  end
32
+
33
+ namespace :examples do
34
+ desc "Run all the examples"
35
+ task :run do
36
+ begin
37
+ require "cutest"
38
+ rescue LoadError
39
+ raise "!! Missing gem `cutest`. Try `gem install cutest`."
40
+ end
41
+
42
+ Cutest.run(Dir["examples/*.rb"])
43
+ end
44
+ end
45
+
data/lib/ohm/key.rb CHANGED
@@ -1,17 +1,35 @@
1
1
  module Ohm
2
2
 
3
- # Represents a key in Redis.
3
+ # Represents a key in Redis. Much of the heavylifting is done using the
4
+ # Nest library.
5
+ #
6
+ # The most important change added by {Ohm::Key} to Nest is the concept of
7
+ # volatile keys, which are used heavily when doing {Ohm::Model::Set#find} or
8
+ # {Ohm::Model::Set#except} operations.
9
+ #
10
+ # A volatile key is simply a key prefixed with `~`. This gives you the
11
+ # benefit if quickly seeing which keys are temporary keys by doing something
12
+ # like:
13
+ #
14
+ # $ redis-cli keys "~*"
15
+ #
16
+ # @see http://github.com/soveran/nest
4
17
  class Key < Nest
5
18
  def volatile
6
19
  self.index("~") == 0 ? self : self.class.new("~", redis)[self]
7
20
  end
8
21
 
22
+ # Produces a key with `other` suffixed with itself. This is primarily
23
+ # used for storing SINTERSTORE results.
9
24
  def +(other)
10
25
  self.class.new("#{self}+#{other}", redis)
11
26
  end
12
27
 
28
+ # Produces a key with `other` suffixed with itself. This is primarily
29
+ # used for storing SDIFFSTORE results.
13
30
  def -(other)
14
31
  self.class.new("#{self}-#{other}", redis)
15
32
  end
16
33
  end
17
34
  end
35
+
@@ -1,7 +1,60 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Ohm
4
+ # Provides a base implementation for extensible validation routines.
5
+ # {Ohm::Validations} currently only provides the following assertions:
6
+ #
7
+ # * assert
8
+ # * assert_present
9
+ # * assert_format
10
+ # * assert_numeric
11
+ #
12
+ # The core tenets that Ohm::Validations advocates can be summed up in a
13
+ # few bullet points:
14
+ #
15
+ # 1. Validations are much simpler and better done using composition rather
16
+ # than macros.
17
+ # 2. Error messages should be kept separate and possibly in the view or
18
+ # presenter layer.
19
+ # 3. It should be easy to write your own validation routine.
20
+ #
21
+ # Since Ohm's philosophy is to keep the core code small, other validations
22
+ # are simply added on a per-model or per-project basis.
23
+ #
24
+ # If you want other validations you may want to take a peek at Ohm::Contrib
25
+ # and all of the validation modules it provides.
26
+ #
27
+ # @see http://cyx.github.com/ohm-contrib/doc/Ohm/WebValidations.html
28
+ # @see http://cyx.github.com/ohm-contrib/doc/Ohm/NumberValidations.html
29
+ # @see http://cyx.github.com/ohm-contrib/doc/Ohm/ExtraValidations.html
30
+ #
31
+ # @example
32
+ #
33
+ # class Product < Ohm::Model
34
+ # attribute :title
35
+ # attribute :price
36
+ # attribute :date
37
+ #
38
+ # def validate
39
+ # assert_present :title
40
+ # assert_numeric :price
41
+ # assert_format :date, /\A[\d]{4}-[\d]{1,2}-[\d]{1,2}\z
42
+ # end
43
+ # end
44
+ #
45
+ # product = Product.new
46
+ # product.valid? == false
47
+ # # => true
48
+ #
49
+ # product.errors == [[:title, :not_present], [:price, :not_numeric],
50
+ # [:date, :format]]
51
+ # # => true
52
+ #
4
53
  module Validations
54
+ # Provides a simple implementation using the Presenter Pattern. When
55
+ # presenting errors, you have to properly catch all errors generated, or
56
+ # else you'll get an {Ohm::Validations::Presenter::UnhandledErrors}
57
+ # exception.
5
58
  class Presenter
6
59
  class UnhandledErrors < StandardError
7
60
  attr :errors
@@ -47,6 +100,8 @@ module Ohm
47
100
  end
48
101
  end
49
102
 
103
+ # A simple class for storing all errors. Since {Ohm::Validations::Errors}
104
+ # extends Array, you can expect all array methods to work on it.
50
105
  class Errors < Array
51
106
  attr_accessor :model
52
107
 
@@ -59,39 +114,100 @@ module Ohm
59
114
  end
60
115
  end
61
116
 
117
+ # Check if the current model state is valid. Each call to {#valid?} will
118
+ # reset the {#errors} array.
119
+ #
120
+ # All model validations should be declared in a `validate` method.
121
+ #
122
+ # @example
123
+ #
124
+ # class Post < Ohm::Model
125
+ # attribute :title
126
+ #
127
+ # def validate
128
+ # assert_present :title
129
+ # end
130
+ # end
131
+ #
62
132
  def valid?
63
133
  errors.clear
64
134
  validate
65
135
  errors.empty?
66
136
  end
67
137
 
138
+ # Base validate implementation.
68
139
  def validate
69
140
  end
70
141
 
142
+ # All errors for this model.
71
143
  def errors
72
144
  @errors ||= Errors.new(self)
73
145
  end
74
146
 
75
147
  protected
76
148
 
149
+ # Allows you to do a validation check against a regular expression.
150
+ # It's important to note that this internally calls {#assert_present},
151
+ # therefore you need not structure your regular expression to check
152
+ # for a non-empty value.
153
+ #
154
+ # @param [Symbol] att The attribute you want to verify the format of.
155
+ # @param [Regexp] format The regular expression with which to compare
156
+ # the value of att with.
157
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
158
+ # when the validation fails.
77
159
  def assert_format(att, format, error = [att, :format])
78
160
  if assert_present(att, error)
79
161
  assert(send(att).to_s.match(format), error)
80
162
  end
81
163
  end
82
164
 
165
+ # The most basic and highly useful assertion. Simply checks if the
166
+ # value of the attribute is empty.
167
+ #
168
+ # @param [Symbol] att The attribute you wish to verify the presence of.
169
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
170
+ # when the validation fails.
83
171
  def assert_present(att, error = [att, :not_present])
84
172
  assert(!send(att).to_s.empty?, error)
85
173
  end
86
174
 
175
+ # Checks if all the characters of an attribute is a digit. If you want to
176
+ # verify that a value is a decimal, try looking at Ohm::Contrib's
177
+ # assert_decimal assertion.
178
+ #
179
+ # @param [Symbol] att The attribute you wish to verify the numeric format.
180
+ # @param [Array<Symbol, Symbol>] error The error that should be returned
181
+ # when the validation fails.
182
+ # @see http://cyx.github.com/ohm-contrib/doc/Ohm/NumberValidations.html
87
183
  def assert_numeric(att, error = [att, :not_numeric])
88
184
  if assert_present(att, error)
89
185
  assert_format(att, /^\d+$/, error)
90
186
  end
91
187
  end
92
188
 
189
+ # The grand daddy of all assertions. If you want to build custom
190
+ # assertions, or even quick and dirty ones, you can simply use this method.
191
+ #
192
+ # @example
193
+ #
194
+ # class Post < Ohm::Model
195
+ # attribute :slug
196
+ # attribute :votes
197
+ #
198
+ # def validate
199
+ # assert_slug :slug
200
+ # assert votes.to_i > 0, [:votes, :not_valid]
201
+ # end
202
+ #
203
+ # protected
204
+ # def assert_slug(att, error = [att, :not_slug])
205
+ # assert send(att).to_s =~ /\A[a-z\-0-9]+\z/, error
206
+ # end
207
+ # end
93
208
  def assert(value, error)
94
209
  value or errors.push(error) && false
95
210
  end
96
211
  end
97
212
  end
213
+
data/lib/ohm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Ohm
4
- VERSION = "0.1.0.rc6"
4
+ VERSION = "0.1.0"
5
5
  end