ohm 0.1.0.rc6 → 0.1.0

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/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