ohm_util 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.
@@ -0,0 +1,28 @@
1
+ require "bench"
2
+ require_relative "../lib/ohm"
3
+
4
+ Ohm.redis = Redic.new("redis://127.0.0.1:6379/15")
5
+ Ohm.flush
6
+
7
+ class Event < Ohm::Model
8
+ attribute :name
9
+ attribute :location
10
+
11
+ index :name
12
+ index :location
13
+ end
14
+
15
+ class Sequence
16
+ def initialize
17
+ @value = 0
18
+ end
19
+
20
+ def succ!
21
+ Thread.exclusive { @value += 1 }
22
+ end
23
+
24
+ def self.[](name)
25
+ @@sequences ||= Hash.new { |hash, key| hash[key] = Sequence.new }
26
+ @@sequences[name]
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "common"
2
+
3
+ benchmark "Create Events" do
4
+ i = Sequence[:events].succ!
5
+
6
+ Event.create(:name => "Redis Meetup #{i}", :location => "London #{i}")
7
+ end
8
+
9
+ benchmark "Find by indexed attribute" do
10
+ Event.find(:name => "Redis Meetup 1").first
11
+ end
12
+
13
+ benchmark "Mass update" do
14
+ Event[1].update(:name => "Redis Meetup II")
15
+ end
16
+
17
+ benchmark "Load events" do
18
+ Event[1].name
19
+ end
20
+
21
+ run 5000
@@ -0,0 +1,13 @@
1
+ require_relative "common"
2
+
3
+ 1000.times do |i|
4
+ Event.create(:name => "Redis Meetup #{i}", :location => "At my place")
5
+ end
6
+
7
+ benchmark "Delete event" do
8
+ Event.all.each do |event|
9
+ event.delete
10
+ end
11
+ end
12
+
13
+ run 1
@@ -0,0 +1,157 @@
1
+ ### Building an activity feed
2
+
3
+ #### Common solutions using a relational design
4
+
5
+ # When faced with this application requirement, the most common approach by
6
+ # far have been to create an *activities* table, and rows in this table would
7
+ # reference a *user*. Activities would typically be generated for each
8
+ # follower (or friend) when a certain user performs an action, like posting a
9
+ # new status update.
10
+
11
+ #### Problems
12
+
13
+ # The biggest issue with this design, is that the *activities* table will
14
+ # quickly get very huge, at which point you would need to shard it on
15
+ # *user_id*. Also, inserting thousands of entries per second would quickly
16
+ # bring your database to its knees.
17
+
18
+ #### Ohm Solution
19
+
20
+ # As always we need to require `Ohm`.
21
+ require "ohm"
22
+
23
+ # We create a `User` class, with a `set` for all the other users he
24
+ # would be `following`, and another `set` for all his `followers`.
25
+ class User < Ohm::Model
26
+ set :followers, User
27
+ set :following, User
28
+
29
+ # Because a `User` literally has a `list` of activities, using a Redis
30
+ # `list` to model the activities would be a good choice. We default to
31
+ # getting the first 100 activities, and use
32
+ # [lrange](http://redis.io/commands/lrange) directly.
33
+ def activities(start = 0, limit = 100)
34
+ redis.call 'LRANGE', key[:activities], start, start + limit
35
+ end
36
+
37
+ # Broadcasting a message to all the `followers` of a user would simply
38
+ # be prepending the message for each if his `followers`. We also use
39
+ # the Redis command
40
+ # [lpush](http://redis.io/commands/lpush) directly.
41
+ def broadcast(str)
42
+ followers.each do |user|
43
+ redis.call 'LPUSH', user.key[:activities], str
44
+ end
45
+ end
46
+
47
+ # Given that *Jane* wants to follow *John*, we simply do the following
48
+ # steps:
49
+ #
50
+ # 1. *John* is added to *Jane*'s `following` list.
51
+ # 2. *Jane* is added to *John*'s `followers` list.
52
+ def follow(other)
53
+ following << other
54
+ other.followers << self
55
+ end
56
+ end
57
+
58
+
59
+ #### Testing
60
+
61
+ # We'll use cutest for our testing framework.
62
+ require "cutest"
63
+
64
+ # The database is flushed before each test.
65
+ prepare { Ohm.flush }
66
+
67
+ # We define two users, `john` and `jane`, and yield them so all
68
+ # other tests are given access to these 2 users.
69
+ setup do
70
+ john = User.create
71
+ jane = User.create
72
+
73
+ [john, jane]
74
+ end
75
+
76
+ # Let's verify our model for `follow`. When `jane` follows `john`,
77
+ # the following conditions should hold:
78
+ #
79
+ # 1. The followers list of `john` is comprised *only* of `jane`.
80
+ # 2. The list of users `jane` is following is comprised *only* of `john`.
81
+ test "jane following john" do |john, jane|
82
+ jane.follow(john)
83
+
84
+ assert_equal [john], jane.following.to_a
85
+ assert_equal [jane], john.followers.to_a
86
+ end
87
+
88
+ # Broadcasting a message should simply notify all the followers of the
89
+ # `broadcaster`.
90
+ test "john broadcasting a message" do |john, jane|
91
+ jane.follow(john)
92
+ john.broadcast("Learning about Redis and Ohm")
93
+
94
+ assert jane.activities.include?("Learning about Redis and Ohm")
95
+ end
96
+
97
+ #### Total Denormalization: Adding HTML
98
+
99
+ # This may be a real edge case design decision, but for some scenarios this
100
+ # may work. The beauty of this solution is that you only have to generate the
101
+ # output once, and successive refreshes of the end user will help you save
102
+ # some CPU cycles.
103
+ #
104
+ # This example of course assumes that the code that generates this does all
105
+ # the conditional checks (possibly changing the point of view like *Me:*
106
+ # instead of *John says:*).
107
+ test "broadcasting the html directly" do |john, jane|
108
+ jane.follow(john)
109
+
110
+ snippet = '<a href="/1">John</a> says: How\'s it going ' +
111
+ '<a href="/user/2">jane</a>?'
112
+
113
+ john.broadcast(snippet)
114
+
115
+ assert jane.activities.include?(snippet)
116
+ end
117
+
118
+ #### Saving Space
119
+
120
+ # In most cases, users don't really care about keeping their entire activity
121
+ # history. This application requirement would be fairly trivial to implement.
122
+
123
+ # Let's reopen our `User` class and define a new broadcast method.
124
+ class User
125
+ # We define a constant where we set the maximum number of activity entries.
126
+ MAX = 10
127
+
128
+ # Using `MAX` as the reference, we truncate the activities feed using
129
+ # [ltrim](http://redis.io/commands/ltrim).
130
+ def broadcast(str)
131
+ followers.each do |user|
132
+ redis.call 'LPUSH', user.key[:activities], str
133
+ redis.call 'LTRIM', user.key[:activities], 0, MAX - 1
134
+ end
135
+ end
136
+ end
137
+
138
+ # Now let's verify that this new behavior is enforced.
139
+ test "pushing 11 activities maintains the list to 10" do |john, jane|
140
+ jane.follow(john)
141
+
142
+ 11.times { john.broadcast("Flooding your feed!") }
143
+
144
+ assert 10 == jane.activities.size
145
+ end
146
+
147
+
148
+ #### Conclusion
149
+
150
+ # As you can see, choosing a more straightforward approach (in this case,
151
+ # actually having a list per user, instead of maintaining a separate
152
+ # `activities` table) will greatly simplify the design of your system.
153
+ #
154
+ # As a final note, keep in mind that the Ohm solution would still need
155
+ # sharding for large datasets, but that would be again trivial to implement
156
+ # using [redis-rb](http://github.com/redis/redis-rb)'s distributed support
157
+ # and sharding it against the *user_id*.
@@ -0,0 +1,162 @@
1
+ ### Chaining Ohm Sets
2
+
3
+ #### Doing the straight forward approach
4
+
5
+ # Let's design our example around the following requirements:
6
+ #
7
+ # 1. a `User` has many orders.
8
+ # 2. an `Order` can be pending, authorized or captured.
9
+ # 3. a `Product` is referenced by an `Order`.
10
+
11
+ #### Doing it the normal way
12
+
13
+ # Let's first require `Ohm`.
14
+ require "ohm"
15
+
16
+ # A `User` has a `collection` of *orders*. Note that a collection
17
+ # is actually just a convenience, which implemented simply will look like:
18
+ #
19
+ # def orders
20
+ # Order.find(user_id: self.id)
21
+ # end
22
+ #
23
+ class User < Ohm::Model
24
+ collection :orders, :Order
25
+ end
26
+
27
+ # The product for our purposes will only contain a name.
28
+ class Product < Ohm::Model
29
+ attribute :name
30
+ end
31
+
32
+ # We define an `Order` with just a single `attribute` called `state`, and
33
+ # also add an `index` so we can search an order given its state.
34
+ #
35
+ # The `reference` to the `User` is actually required for the `collection`
36
+ # of *orders* in the `User` declared above, because the `reference` defines
37
+ # an index called `:user_id`.
38
+ #
39
+ # We also define a `reference` to a `Product`.
40
+ class Order < Ohm::Model
41
+ attribute :state
42
+ index :state
43
+
44
+ reference :user, User
45
+ reference :product, Product
46
+ end
47
+
48
+ ##### Testing what we have so far.
49
+
50
+ # For the purposes of this tutorial, we'll use cutest for our test framework.
51
+ require "cutest"
52
+
53
+ # Make sure that every run of our test suite has a clean Redis instance.
54
+ prepare { Ohm.flush }
55
+
56
+ # Let's create a *user*, a *pending*, *authorized* and a captured order.
57
+ # We also create two products named *iPod* and *iPad*.
58
+ setup do
59
+ @user = User.create
60
+
61
+ @ipod = Product.create(name: "iPod")
62
+ @ipad = Product.create(name: "iPad")
63
+
64
+ @pending = Order.create(user: @user, state: "pending",
65
+ product: @ipod)
66
+ @authorized = Order.create(user: @user, state: "authorized",
67
+ product: @ipad)
68
+ @captured = Order.create(user: @user, state: "captured",
69
+ product: @ipad)
70
+ end
71
+
72
+ # Now let's try and grab all pending orders, and also pending
73
+ # *iPad* and *iPod* ones.
74
+ test "finding pending orders" do
75
+ assert @user.orders.find(state: "pending").include?(@pending)
76
+
77
+ assert @user.orders.find(state: "pending",
78
+ product_id: @ipod.id).include?(@pending)
79
+
80
+ assert @user.orders.find(state: "pending",
81
+ product_id: @ipad.id).empty?
82
+ end
83
+
84
+ # Now we try and find captured and/or authorized orders.
85
+ test "finding authorized and/or captured orders" do
86
+ assert @user.orders.find(state: "authorized").include?(@authorized)
87
+ assert @user.orders.find(state: "captured").include?(@captured)
88
+
89
+ results = @user.orders.find(state: "authorized").union(state: "captured")
90
+
91
+ assert results.include?(@authorized)
92
+ assert results.include?(@captured)
93
+ end
94
+
95
+ #### Creating shortcuts
96
+
97
+ # You can of course define methods to make that code more readable.
98
+ class User < Ohm::Model
99
+ def authorized_orders
100
+ orders.find(state: "authorized")
101
+ end
102
+
103
+ def captured_orders
104
+ orders.find(state: "captured")
105
+ end
106
+ end
107
+
108
+ # And we can now test these new methods.
109
+ test "finding authorized and/or captured orders" do
110
+ assert @user.authorized_orders.include?(@authorized)
111
+ assert @user.captured_orders.include?(@captured)
112
+ end
113
+
114
+ # In most cases this is fine, but if you want to have a little fun,
115
+ # then we can play around with some chainability using scopes.
116
+
117
+ # Let's require `ohm-contrib`, which we will be using for scopes.
118
+ require "ohm/contrib"
119
+
120
+ # Include `Ohm::Scope` module and desired scopes.
121
+ class Order
122
+ include Ohm::Scope
123
+
124
+ scope do
125
+ def pending
126
+ find(state: "pending")
127
+ end
128
+
129
+ def authorized
130
+ find(state: "authorized")
131
+ end
132
+
133
+ def captured
134
+ find(state: "captured")
135
+ end
136
+
137
+ def accepted
138
+ find(state: "authorized").union(state: "captured")
139
+ end
140
+ end
141
+ end
142
+
143
+ # Ok! Let's put all of that chaining code to good use.
144
+ test "finding pending orders using a chainable style" do
145
+ assert @user.orders.pending.include?(@pending)
146
+ assert @user.orders.pending.find(product_id: @ipod.id).include?(@pending)
147
+
148
+ assert @user.orders.pending.find(product_id: @ipad.id).empty?
149
+ end
150
+
151
+ test "finding authorized and/or captured orders using a chainable style" do
152
+ assert @user.orders.authorized.include?(@authorized)
153
+ assert @user.orders.captured.include?(@captured)
154
+
155
+ assert @user.orders.accepted.include?(@authorized)
156
+ assert @user.orders.accepted.include?(@captured)
157
+
158
+ accepted = @user.orders.accepted
159
+
160
+ assert accepted.find(product_id: @ipad.id).include?(@authorized)
161
+ assert accepted.find(product_id: @ipad.id).include?(@captured)
162
+ end
@@ -0,0 +1,75 @@
1
+ ### Make Peace wih JSON and Hash
2
+
3
+ #### Why do I care?
4
+
5
+ # If you've ever needed to build an AJAX route handler, you may have noticed
6
+ # the prevalence of the design pattern where you return a JSON response.
7
+ #
8
+ # on get, "comments" do
9
+ # res.write Comment.all.to_json
10
+ # end
11
+ #
12
+ # `Ohm` helps you here by providing sensible defaults. It's not very popular,
13
+ # but `Ohm` actually has a `to_hash` method.
14
+
15
+ # Let's start by requiring `ohm` and `ohm/json`.
16
+ require "ohm"
17
+ require "ohm/json"
18
+
19
+ # Here we define our `Post` model with just a single `attribute` called `title`.
20
+ class Post < Ohm::Model
21
+ attribute :title
22
+ end
23
+
24
+ # Now let's load the test framework `cutest` to test our code.
25
+ require "cutest"
26
+
27
+ # We also call `Ohm.flush` for each test run.
28
+ prepare { Ohm.flush }
29
+
30
+ # When we successfully create a `Post`, we can see that it returns
31
+ # only the *id* and its value in the hash.
32
+ test "hash representation when created" do
33
+ post = Post.create(title: "my post")
34
+
35
+ assert_equal Hash[id: post.id], post.to_hash
36
+ end
37
+
38
+ # The JSON representation is actually just `post.to_hash.to_json`, so the
39
+ # same result, only in JSON, is returned.
40
+ test "json representation when created" do
41
+ post = Post.create(title: "my post")
42
+
43
+ assert_equal "{\"id\":\"#{post.id}\"}", post.to_json
44
+ end
45
+
46
+ #### Whitelisted approach
47
+
48
+ # Unlike other frameworks which dumps out all attributes by default,
49
+ # `Ohm` favors a whitelisted approach where you have to explicitly
50
+ # declare which attributes you want.
51
+ #
52
+ # By default, only `:id` will be available if the model is persisted.
53
+
54
+ # Let's re-open our Post class, and add a `to_hash` method.
55
+ class Post
56
+ def to_hash
57
+ super.merge(title: title)
58
+ end
59
+ end
60
+
61
+ # Now, let's test that the title is in fact part of `to_hash`.
62
+ test "customized to_hash" do
63
+ post = Post.create(title: "Override FTW?")
64
+
65
+ assert_equal Hash[id: post.id, title: post.title], post.to_hash
66
+ end
67
+
68
+ #### Conclusion
69
+
70
+ # Ohm has a lot of neat intricacies like this. Some of the things to keep
71
+ # in mind from this tutorial would be:
72
+ #
73
+ # 1. `Ohm` doesn't assume too much about your needs.
74
+ # 2. If you need a customized version, you can always define it yourself.
75
+ # 3. Customization is easy using basic OOP principles.