sohm 0.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/examples/slug.rb ADDED
@@ -0,0 +1,149 @@
1
+ ### All Kinds of Slugs
2
+
3
+ # The problem of making semantic URLs have definitely been a prevalent one.
4
+ # There has been quite a lot of solutions around this theme, so we'll discuss
5
+ # a few simple ways to handle slug generation.
6
+
7
+ #### ID Prefixed slugs
8
+
9
+ # This is by far the simplest (and most cost-effective way) of generating
10
+ # slugs. Implementing this is pretty simple too.
11
+
12
+ # Let's first require `Ohm`.
13
+ require "ohm"
14
+
15
+ # Now let's define our `Post` model, with just a single
16
+ # `attribute` *title*.
17
+ class Post < Ohm::Model
18
+ attribute :title
19
+
20
+ # To make it more convenient, we override the finder syntax,
21
+ # so doing a `Post["1-my-post-title"]` will in effect just call
22
+ # `Post[1]`.
23
+ def self.[](id)
24
+ super(id.to_i)
25
+ end
26
+
27
+ # This pattern was mostly borrowed from Rails' style of generating
28
+ # URLs. Here we just concatenate the `id` and a sanitized form
29
+ # of our title.
30
+ def to_param
31
+ "#{id}-#{title.to_s.gsub(/\p{^Alnum}/u, " ").gsub(/\s+/, "-").downcase}"
32
+ end
33
+ end
34
+
35
+ # Let's verify our code using the
36
+ # [Cutest](http://github.com/djanowski/cutest)
37
+ # testing framework.
38
+ require "cutest"
39
+
40
+ # Also we ensure every test run is guaranteed to have a clean
41
+ # *Redis* instance.
42
+ prepare { Ohm.flush }
43
+
44
+ # For each and every test, we create a post with
45
+ # the title "ID Prefixed Slugs". Since it's the last
46
+ # line of our `setup`, it will also be yielded to
47
+ # each of our test blocks.
48
+ setup do
49
+ Post.create(:title => "ID Prefixed Slugs")
50
+ end
51
+
52
+ # Now let's verify the behavior of our `to_param` method.
53
+ # Note that we make it dash-separated and lowercased.
54
+ test "to_param" do |post|
55
+ assert "1-id-prefixed-slugs" == post.to_param
56
+ end
57
+
58
+ # We also check that our easier finder syntax works.
59
+ test "finding the post" do |post|
60
+ assert post == Post[post.to_param]
61
+ end
62
+
63
+ #### We don't have to code it everytime
64
+
65
+ # Because of the prevalence, ease of use, and efficiency of this style of slug
66
+ # generation, it has been extracted to a module in
67
+ # [Ohm::Contrib](http://github.com/cyx/ohm-contrib/) called `Ohm::Slug`.
68
+
69
+ # Let's create a different model to demonstrate how to use it.
70
+ # (Run `[sudo] gem install ohm-contrib` to install ohm-contrib).
71
+
72
+ # When using `ohm-contrib`, we simply require it, and then
73
+ # directly reference the specific module. In this case, we
74
+ # use `Ohm::Slug`.
75
+ require "ohm/contrib"
76
+
77
+ class Video < Ohm::Model
78
+ include Ohm::Slug
79
+
80
+ attribute :title
81
+
82
+ # `Ohm::Slug` just uses the value of the object's `to_s`.
83
+ def to_s
84
+ title.to_s
85
+ end
86
+ end
87
+
88
+ # Now to quickly verify that everything works similar to our
89
+ # example above!
90
+ test "video slugging" do
91
+ video = Video.create(:title => "A video about ohm")
92
+
93
+ assert "1-a-video-about-ohm" == video.to_param
94
+ assert video == Video[video.id]
95
+ end
96
+
97
+ # That's it, and it works similarly to the example above.
98
+
99
+ #### What if I want a slug without an ID prefix?
100
+
101
+ # For this case, we can still make use of `Ohm::Slug`'s ability to
102
+ # make a clean string.
103
+
104
+ # Let's create an `Article` class which has a single attribute `title`.
105
+ class Article < Ohm::Model
106
+ include Ohm::Callbacks
107
+
108
+ attribute :title
109
+
110
+ # Now before creating this object, we just call `Ohm::Slug.slug` directly.
111
+ # We also check if the generated slug exists, and repeatedly try
112
+ # appending numbers.
113
+ protected
114
+ def before_create
115
+ temp = Ohm::Slug.slug(title)
116
+ self.id = temp
117
+
118
+ counter = 0
119
+ while Article.exists?(id)
120
+ self.id = "%s-%d" % [temp, counter += 1]
121
+ end
122
+ end
123
+ end
124
+
125
+ # We now verify the behavior of our `Article` class
126
+ # by creating an article with the same title 3 times.
127
+ test "create an article with the same title" do
128
+ a1 = Article.create(:title => "All kinds of slugs")
129
+ a2 = Article.create(:title => "All kinds of slugs")
130
+ a3 = Article.create(:title => "All kinds of slugs")
131
+
132
+ assert a1.id == "all-kinds-of-slugs"
133
+ assert a2.id == "all-kinds-of-slugs-1"
134
+ assert a3.id == "all-kinds-of-slugs-2"
135
+ end
136
+
137
+ #### Conclusion
138
+
139
+ # Slug generation comes in all different flavors.
140
+ #
141
+ # 1. The first solution is good enough for most cases. The primary advantage
142
+ # of this solution is that we don't have to check for ID clashes.
143
+ #
144
+ # 2. The second solution may be needed for cases where you must make
145
+ # the URLs absolutely clean and readable, and you hate having those
146
+ # number prefixes.
147
+ #
148
+ # *NOTE:* The example we used for the second solution has potential
149
+ # race conditions. I'll leave fixing it as an exercise to you.
@@ -0,0 +1,237 @@
1
+ ### Tagging
2
+
3
+ #### Intro
4
+
5
+ # When building a Web 2.0 application, tagging will probably come up
6
+ # as one of the most requested features. Popularized by Delicious,
7
+ # it has quickly become a useful way to organize crowd sourced data.
8
+
9
+ #### How it was done
10
+
11
+ # Typically, when you do tagging using an RDBMS, you'll probably end up
12
+ # having a taggings and a tags table, hence a many-to-many design.
13
+ # Here is a quick sketch just to illustrate:
14
+ #
15
+ #
16
+ #
17
+ # Post Taggings Tag
18
+ # ---- -------- ---
19
+ # id tag_id id
20
+ # title post_id name
21
+ #
22
+ # As you can see, this design leads to a lot of problems:
23
+ #
24
+ # 1. Trying to find the tags of a post will have to go through taggings, and
25
+ # then individually find the actual tag.
26
+ # 2. One might be inclined to use a JOIN query, but we all know
27
+ # [joins are evil](http://stackoverflow.com/questions/1020847).
28
+ # 3. Building a tag cloud or some form of tag ranking is unintuitive.
29
+
30
+ #### The Ohm approach
31
+
32
+ # Here is a basic outline of what we'll need:
33
+ #
34
+ # 1. We should be able to tag a post (separated by commas).
35
+ # 2. We should be able to find a post with a given tag.
36
+
37
+ #### Beginning with our Post model
38
+
39
+ # Let's first require ohm.
40
+ require 'ohm'
41
+
42
+ # We then declare our class, inheriting from `Ohm::Model` in the process.
43
+ class Post < Ohm::Model
44
+
45
+ # The structure, fields, and other associations are defined in a declarative
46
+ # manner. Ohm allows us to declare *attributes*, *sets*, *lists* and
47
+ # *counters*. For our usecase here, only two *attributes* will get the job
48
+ # done. The `body` will just
49
+ # be a plain string, and the `tags` will contain our comma-separated list of
50
+ # words, i.e. "ruby, redis, ohm". We then declare an `index` (which can be
51
+ # an `attribute` or just a plain old method), which we point to our method
52
+ # `tag`.
53
+ attribute :body
54
+ attribute :tags
55
+ index :tag
56
+
57
+ # One very interesting thing about Ohm indexes is that it can either be a
58
+ # *String* or an *Enumerable* data structure. When we declare it as an
59
+ # *Enumerable*, `Ohm` will create an index for every element. So if `tag`
60
+ # returned `[ruby, redis, ohm]` then we can search it using any of the
61
+ # following:
62
+ #
63
+ # 1. ruby
64
+ # 2. redis
65
+ # 3. ohm
66
+ # 4. ruby, redis
67
+ # 5. ruby, ohm
68
+ # 6. redis, ohm
69
+ # 7. ruby, redis, ohm
70
+ #
71
+ # Pretty neat ain't it?
72
+ def tag
73
+ tags.to_s.split(/\s*,\s*/).uniq
74
+ end
75
+ end
76
+
77
+ #### Testing it out
78
+
79
+ # It's a very good habit to test all the time. In the Ruby community,
80
+ # a lot of test frameworks have been created.
81
+
82
+ # For our purposes in this example, we'll use cutest.
83
+ require "cutest"
84
+
85
+ # Cutest allows us to define callbacks which are guaranteed to be executed
86
+ # every time a new `test` begins. Here, we just make sure that the Redis
87
+ # instance of `Ohm` is empty everytime.
88
+ prepare { Ohm.flush }
89
+
90
+ # Next, let's create a simple `Post` instance. The return value of the `setup`
91
+ # block will be passed to every `test` block, so we don't actually have to
92
+ # assign it to an instance variable.
93
+ setup do
94
+ Post.create(:body => "Ohm Tagging", :tags => "tagging, ohm, redis")
95
+ end
96
+
97
+ # For our first run, let's verify the fact that we can find a `Post`
98
+ # using any of the tags we gave.
99
+ test "find using a single tag" do |p|
100
+ assert Post.find(tag: "tagging").include?(p)
101
+ assert Post.find(tag: "ohm").include?(p)
102
+ assert Post.find(tag: "redis").include?(p)
103
+ end
104
+
105
+ # Now we verify our claim earlier, that it is possible to find a tag
106
+ # using any one of the combinations for the given set of tags.
107
+ #
108
+ # We also verify that if we pass in a non-existent tag name that
109
+ # we'll fail to find the `Post` we just created.
110
+ test "find using an intersection of multiple tag names" do |p|
111
+ assert Post.find(tag: ["tagging", "ohm"]).include?(p)
112
+ assert Post.find(tag: ["tagging", "redis"]).include?(p)
113
+ assert Post.find(tag: ["ohm", "redis"]).include?(p)
114
+ assert Post.find(tag: ["tagging", "ohm", "redis"]).include?(p)
115
+
116
+ assert ! Post.find(tag: ["tagging", "foo"]).include?(p)
117
+ end
118
+
119
+ #### Adding a Tag model
120
+
121
+ # Let's pretend that the client suddenly requested that we keep track
122
+ # of the number of times a tag has been used. It's a pretty fair requirement
123
+ # after all. Updating our requirements, we will now have:
124
+ #
125
+ # 1. We should be able to tag a post (separated by commas).
126
+ # 2. We should be able to find a post with a given tag.
127
+ # 3. We should be able to find top tags, and their count.
128
+
129
+ # Continuing from our example above, let's require `ohm-contrib`, which we
130
+ # will be using for callbacks.
131
+ require "ohm/contrib"
132
+
133
+ # Let's quickly re-open our Post class.
134
+ class Post
135
+ # When we want our class to have extended functionality like callbacks,
136
+ # we simply include the necessary modules, in this case `Ohm::Callbacks`,
137
+ # which will be responsible for inserting `before_*` and `after_*` methods
138
+ # in the object's lifecycle.
139
+ include Ohm::Callbacks
140
+
141
+ # To make our code more concise, we just quickly change our implementation
142
+ # of `tag` to receive a default parameter:
143
+ def tag(tags = self.tags)
144
+ tags.to_s.split(/\s*,\s*/).uniq
145
+ end
146
+
147
+ # For all but the most simple cases, we would probably need to define
148
+ # callbacks. When we included `Ohm::Callbacks` above, it actually gave us
149
+ # the following:
150
+ #
151
+ # 1. `before_validate` and `after_validate`
152
+ # 2. `before_create` and `after_create`
153
+ # 3. `before_update` and `after_update`
154
+ # 4. `before_save` and `after_save`
155
+ # 5. `before_delete` and `after_delete`
156
+
157
+ # For our scenario, we only need a `before_update` and `after_save`.
158
+ # The idea for our `before_update` is to decrement the `total` of
159
+ # all existing tags. We use `get(:tags)` the original tags for the
160
+ # record and use assigned one on save.
161
+ protected
162
+ def before_update
163
+ assigned_tags = tags
164
+ tag(get(:tags)).map(&Tag).each { |t| t.decr :total }
165
+ self.tags = assigned_tags
166
+ end
167
+
168
+ # And of course, we increment all new tags for a particular record
169
+ # after successfully saving it.
170
+ def after_save
171
+ tag.map(&Tag).each { |t| t.incr :total }
172
+ end
173
+ end
174
+
175
+ #### Our Tag model
176
+
177
+ # The `Tag` model has only one type, which is a `counter` for the `total`.
178
+ # Since `Ohm` allows us to use any kind of ID (not just numeric sequences),
179
+ # we can actually use the tag name to identify a `Tag`.
180
+ class Tag < Ohm::Model
181
+ counter :total
182
+
183
+ # The syntax for finding a record by its ID is `Tag["ruby"]`. The standard
184
+ # behavior in `Ohm` is to return `nil` when the ID does not exist.
185
+ #
186
+ # To simplify our code, we override `Tag["ruby"]`, and make it create a
187
+ # new `Tag` if it doesn't exist yet. One important implementation detail
188
+ # though is that we need to encode the tag name, so special characters
189
+ # and spaces won't produce an invalid key.
190
+ def self.[](id)
191
+ encoded_id = id.encode
192
+ super(encoded_id) || create(:id => encoded_id)
193
+ end
194
+ end
195
+
196
+ #### Verifying our third requirement
197
+
198
+ # Continuing from our test cases above, let's add test coverage for the
199
+ # behavior of counting tags.
200
+
201
+ # For each and every tag we initially create, we need to make sure they have a
202
+ # total of 1.
203
+ test "verify total to be exactly 1" do
204
+ assert 1 == Tag["ohm"].total
205
+ assert 1 == Tag["redis"].total
206
+ assert 1 == Tag["tagging"].total
207
+ end
208
+
209
+ # If we try and create another post tagged "ruby", "redis", `Tag["redis"]`
210
+ # should then have a total of 2. All of the other tags will still have
211
+ # a total of 1.
212
+ test "verify totals increase" do
213
+ Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
214
+
215
+ assert 1 == Tag["ohm"].total
216
+ assert 1 == Tag["tagging"].total
217
+ assert 1 == Tag["ruby"].total
218
+ assert 2 == Tag["redis"].total
219
+ end
220
+
221
+ # Finally, let's verify the scenario where we create a `Post` tagged
222
+ # "ruby", "redis" and update it to only have the tag "redis",
223
+ # effectively removing the tag "ruby" from our `Post`.
224
+ test "updating an existing post decrements the tags removed" do
225
+ p = Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
226
+ p.update(:tags => "redis")
227
+
228
+ assert 0 == Tag["ruby"].total
229
+ assert 2 == Tag["redis"].total
230
+ end
231
+
232
+ ## Conclusion
233
+
234
+ # Most of the time we tend to think in terms of an RDBMS way, and this is in
235
+ # no way a negative thing. However, it is important to try and switch your
236
+ # frame of mind when working with Ohm (and Redis) because it will greatly save
237
+ # you time, and possibly lead to a great design.
data/lib/sample.rb ADDED
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+ require 'sohm'
3
+
4
+ class User < Sohm::Model
5
+ attribute :age
6
+ attribute :name
7
+
8
+ serial_attribute :desc
9
+ serial_attribute :list
10
+
11
+ index :age
12
+ index :desc
13
+ index :list
14
+ end
@@ -0,0 +1,51 @@
1
+ module Ohm
2
+ class Command
3
+ def self.[](operation, head, *tail)
4
+ return head if tail.empty?
5
+
6
+ new(operation, head, *tail)
7
+ end
8
+
9
+ attr :operation
10
+ attr :args
11
+ attr :keys
12
+
13
+ def initialize(operation, *args)
14
+ @operation = operation
15
+ @args = args
16
+ @keys = []
17
+ end
18
+
19
+ def call(nido, redis)
20
+ newkey(nido, redis) do |key|
21
+ redis.call(@operation, key, *params(nido, redis))
22
+ end
23
+ end
24
+
25
+ def clean
26
+ keys.each do |key, redis|
27
+ redis.call("DEL", key)
28
+ end
29
+
30
+ subcommands.each { |cmd| cmd.clean }
31
+ end
32
+
33
+ private
34
+ def subcommands
35
+ args.select { |arg| arg.respond_to?(:call) }
36
+ end
37
+
38
+ def params(nido, redis)
39
+ args.map { |arg| arg.respond_to?(:call) ? arg.call(nido, redis) : arg }
40
+ end
41
+
42
+ def newkey(nido, redis)
43
+ key = nido[SecureRandom.hex(32)]
44
+ keys << [key, redis]
45
+
46
+ yield key
47
+
48
+ return key
49
+ end
50
+ end
51
+ end
data/lib/sohm/json.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "json"
2
+
3
+ module Ohm
4
+ class Model
5
+ # Export a JSON representation of the model by encoding `to_hash`.
6
+ def to_json(*args)
7
+ to_hash.to_json(*args)
8
+ end
9
+ end
10
+
11
+ module Collection
12
+ # Sugar for to_a.to_json for all types of Sets
13
+ def to_json(*args)
14
+ to_a.to_json(*args)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ -- This script receives three parameters, all encoded with
2
+ -- MessagePack. The decoded values are used for deleting a model
3
+ -- instance in Redis and removing any reference to it in sets
4
+ -- (indices) and hashes (unique indices).
5
+ --
6
+ -- # model
7
+ --
8
+ -- Table with three attributes:
9
+ -- id (model instance id)
10
+ -- key (hash where the attributes will be saved)
11
+ -- name (model name)
12
+ --
13
+ -- # uniques
14
+ --
15
+ -- Fields and values to be removed from the unique indices.
16
+ --
17
+ -- # tracked
18
+ --
19
+ -- Keys that share the lifecycle of this model instance, that
20
+ -- should be removed as this object is deleted.
21
+ --
22
+ local model = cmsgpack.unpack(ARGV[1])
23
+ local uniques = cmsgpack.unpack(ARGV[2])
24
+ local tracked = cmsgpack.unpack(ARGV[3])
25
+
26
+ local function remove_indices(model)
27
+ local memo = model.key .. ":_indices"
28
+ local existing = redis.call("SMEMBERS", memo)
29
+
30
+ for _, key in ipairs(existing) do
31
+ redis.call("SREM", key, model.id)
32
+ redis.call("SREM", memo, key)
33
+ end
34
+ end
35
+
36
+ local function remove_uniques(model, uniques)
37
+ local memo = model.key .. ":_uniques"
38
+
39
+ for field, _ in pairs(uniques) do
40
+ local key = model.name .. ":uniques:" .. field
41
+
42
+ redis.call("HDEL", key, redis.call("HGET", memo, key))
43
+ redis.call("HDEL", memo, key)
44
+ end
45
+ end
46
+
47
+ local function remove_tracked(model, tracked)
48
+ for _, tracked_key in ipairs(tracked) do
49
+ local key = model.key .. ":" .. tracked_key
50
+
51
+ redis.call("DEL", key)
52
+ end
53
+ end
54
+
55
+ local function delete(model)
56
+ local keys = {
57
+ model.key .. ":counters",
58
+ model.key .. ":_indices",
59
+ model.key .. ":_uniques",
60
+ model.key
61
+ }
62
+
63
+ redis.call("SREM", model.name .. ":all", model.id)
64
+ redis.call("DEL", unpack(keys))
65
+ end
66
+
67
+ remove_indices(model)
68
+ remove_uniques(model, uniques)
69
+ remove_tracked(model, tracked)
70
+ delete(model)
71
+
72
+ return model.id
@@ -0,0 +1,13 @@
1
+ local ctoken = redis.call('HGET', KEYS[1], '_cas')
2
+ if (not ctoken) or ctoken == ARGV[2] then
3
+ local ntoken
4
+ if not ctoken then
5
+ ntoken = 1
6
+ else
7
+ ntoken = tonumber(ctoken) + 1
8
+ end
9
+ redis.call('HMSET', KEYS[1], '_sdata', ARGV[1], '_cas', ntoken)
10
+ return ntoken
11
+ else
12
+ error('cas_error')
13
+ end