ohm 0.1.5 → 1.0.0.alpha1

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,129 @@
1
+ require "set"
2
+
3
+ module Ohm
4
+
5
+ # Transactions in Ohm are designed to be composable and atomic. They use
6
+ # Redis WATCH/MULTI/EXEC to perform the comands sequentially but in a single
7
+ # step.
8
+ #
9
+ # @example
10
+ #
11
+ # redis = Ohm.redis
12
+ #
13
+ # t1 = Ohm::Transaction.new do |t|
14
+ # s = nil
15
+ #
16
+ # t.watch("foo")
17
+ #
18
+ # t.read do
19
+ # s = redis.type("foo")
20
+ # end
21
+ #
22
+ # t.write do
23
+ # redis.set("foo", s)
24
+ # end
25
+ # end
26
+ #
27
+ # t2 = Ohm::Transaction.new do |t|
28
+ # t.watch("foo")
29
+ #
30
+ # t.write do
31
+ # redis.set("foo", "bar")
32
+ # end
33
+ # end
34
+ #
35
+ # # Compose transactions by passing them to Ohm::Transaction.new.
36
+ # t3 = Ohm::Transaction.new(t1, t2)
37
+ # t3.commit(redis)
38
+ #
39
+ # # Compose transactions by appending them.
40
+ # t1.append(t2)
41
+ # t1.commit(redis)
42
+ #
43
+ # @see http://redis.io/topic/transactions Transactions in Redis.
44
+ class Transaction
45
+ class Store < BasicObject
46
+ class EntryAlreadyExistsError < ::RuntimeError
47
+ end
48
+
49
+ def method_missing(writer, value)
50
+ super unless writer[-1] == "="
51
+
52
+ reader = writer[0..-2].to_sym
53
+
54
+ __metaclass__.send(:define_method, reader) do
55
+ value
56
+ end
57
+
58
+ __metaclass__.send(:define_method, writer) do |*_|
59
+ ::Kernel.raise EntryAlreadyExistsError
60
+ end
61
+ end
62
+
63
+ private
64
+ def __metaclass__
65
+ class << self; self end
66
+ end
67
+ end
68
+
69
+ attr :phase
70
+
71
+ def initialize
72
+ @phase = Hash.new { |h, k| h[k] = ::Set.new }
73
+
74
+ yield self if block_given?
75
+ end
76
+
77
+ def append(t)
78
+ t.phase.each do |key, values|
79
+ phase[key].merge(values)
80
+ end
81
+
82
+ self
83
+ end
84
+
85
+ def watch(*keys)
86
+ phase[:watch] += keys
87
+ end
88
+
89
+ def read(&block)
90
+ phase[:read] << block
91
+ end
92
+
93
+ def write(&block)
94
+ phase[:write] << block
95
+ end
96
+
97
+ def before(&block)
98
+ phase[:before] << block
99
+ end
100
+
101
+ def after(&block)
102
+ phase[:after] << block
103
+ end
104
+
105
+ def commit(db)
106
+ phase[:before].each(&:call)
107
+
108
+ loop do
109
+ store = Store.new
110
+
111
+ if phase[:watch].any?
112
+ db.watch(*phase[:watch])
113
+ end
114
+
115
+ run(phase[:read], store)
116
+
117
+ break if db.multi do
118
+ run(phase[:write], store)
119
+ end
120
+ end
121
+
122
+ phase[:after].each(&:call)
123
+ end
124
+
125
+ def run(procs, store)
126
+ procs.each { |p| p.call(store) }
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path("./helper", File.dirname(__FILE__))
2
+
3
+ class User < Ohm::Model
4
+ collection :posts, :Post
5
+ end
6
+
7
+ class Post < Ohm::Model
8
+ reference :user, :User
9
+ end
10
+
11
+ setup do
12
+ u = User.create
13
+ p = Post.create(user: u)
14
+
15
+ [u, p]
16
+ end
17
+
18
+ test "basic shake and bake" do |u, p|
19
+ assert u.posts.include?(p)
20
+
21
+ p = Post[p.id]
22
+ assert_equal u, p.user
23
+ end
24
+
25
+ test "memoization" do |u, p|
26
+ # This will read the user instance once.
27
+ p.user
28
+ assert_equal p.user, p.instance_variable_get(:@_memo)[:user]
29
+
30
+ # This will un-memoize the user instance
31
+ p.user = u
32
+ assert_equal nil, p.instance_variable_get(:@_memo)[:user]
33
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ prepare.clear
6
+
7
+ test "connects lazily" do
8
+ Ohm.connect(:port => 9876)
9
+
10
+ begin
11
+ Ohm.redis.get "foo"
12
+ rescue => e
13
+ assert_equal Redis::CannotConnectError, e.class
14
+ end
15
+ end
16
+
17
+ test "provides a separate connection for each thread" do
18
+ assert Ohm.redis == Ohm.redis
19
+
20
+ conn1, conn2 = nil
21
+
22
+ threads = []
23
+
24
+ threads << Thread.new do
25
+ conn1 = Ohm.redis
26
+ end
27
+
28
+ threads << Thread.new do
29
+ conn2 = Ohm.redis
30
+ end
31
+
32
+ threads.each { |t| t.join }
33
+
34
+ assert conn1 != conn2
35
+ end
36
+
37
+ test "supports connecting by URL" do
38
+ Ohm.connect(:url => "redis://localhost:9876")
39
+
40
+ begin
41
+ Ohm.redis.get "foo"
42
+ rescue => e
43
+ assert_equal Redis::CannotConnectError, e.class
44
+ end
45
+ end
46
+
47
+ setup do
48
+ Ohm.connect(:url => "redis://localhost:6379/0")
49
+ end
50
+
51
+ test "connection class" do
52
+ conn = Ohm::Connection.new(:foo, :url => "redis://localhost:6379/0")
53
+
54
+ assert conn.redis.kind_of?(Redis)
55
+ end
56
+
57
+ test "model can define its own connection" do
58
+ class B < Ohm::Model
59
+ connect(:url => "redis://localhost:6379/1")
60
+ end
61
+
62
+ assert_equal B.conn.options, {:url=>"redis://localhost:6379/1"}
63
+ assert_equal Ohm.conn.options, {:url=>"redis://localhost:6379/0"}
64
+ end
65
+
66
+ test "model inherits Ohm.redis connection by default" do
67
+ Ohm.connect(:url => "redis://localhost:9876")
68
+ class C < Ohm::Model
69
+ end
70
+
71
+ assert_equal C.conn.options, Ohm.conn.options
72
+ end
data/test/core.rb ADDED
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ class Event < Ohm::Model
6
+ attribute :name
7
+ attribute :location
8
+ end
9
+
10
+ test "assign attributes from the hash" do
11
+ event = Event.new(:name => "Ruby Tuesday")
12
+ assert_equal event.name, "Ruby Tuesday"
13
+ end
14
+
15
+ test "assign an ID and save the object" do
16
+ event1 = Event.create(:name => "Ruby Tuesday")
17
+ event2 = Event.create(:name => "Ruby Meetup")
18
+
19
+ assert_equal "1", event1.id
20
+ assert_equal "2", event2.id
21
+ end
22
+
23
+ test "save the attributes in UTF8" do
24
+ event = Event.create(:name => "32° Kisei-sen")
25
+ assert "32° Kisei-sen" == Event[event.id].name
26
+ end
data/test/counters.rb ADDED
@@ -0,0 +1,67 @@
1
+ require File.expand_path("./helper", File.dirname(__FILE__))
2
+
3
+ $VERBOSE = false
4
+
5
+ class Ad < Ohm::Model
6
+ end
7
+
8
+ test "counters aren't overwritten by competing saves" do
9
+ Ad.counter :hits
10
+
11
+ instance1 = Ad.create
12
+ instance1.incr :hits
13
+
14
+ instance2 = Ad[instance1.id]
15
+
16
+ instance1.incr :hits
17
+ instance1.incr :hits
18
+
19
+ instance2.save
20
+
21
+ instance1 = Ad[instance1.id]
22
+ assert_equal 3, instance1.hits
23
+ end
24
+
25
+ test "you can increment counters even when attributes is empty" do
26
+ Ad.counter :hits
27
+
28
+ ad = Ad.create
29
+ ad = Ad[ad.id]
30
+
31
+ ex = nil
32
+
33
+ begin
34
+ ad.incr :hits
35
+ rescue ArgumentError => e
36
+ ex = e
37
+ end
38
+
39
+ assert_equal nil, ex
40
+ end
41
+
42
+ test "an attribute gets saved properly" do
43
+ Ad.attribute :name
44
+ Ad.counter :hits
45
+
46
+ ad = Ad.create(name: "foo")
47
+ ad.incr :hits, 10
48
+ assert_equal 10, ad.hits
49
+
50
+ # Now let's just load and save it.
51
+ ad = Ad[ad.id]
52
+ ad.save
53
+
54
+ # The attributes should remain the same
55
+ ad = Ad[ad.id]
56
+ assert_equal "foo", ad.name
57
+ assert_equal 10, ad.hits
58
+
59
+ # If we load and save again while we incr behind the scenes,
60
+ # the latest counter values should be respected.
61
+ ad = Ad[ad.id]
62
+ ad.incr :hits, 5
63
+ ad.save
64
+
65
+ ad = Ad[ad.id]
66
+ assert_equal 15, ad.hits
67
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ if defined?(Ohm::Model::PureRuby)
6
+ class User < Ohm::Model
7
+ attribute :email
8
+
9
+ attr_accessor :foo
10
+
11
+ def save
12
+ super do |t|
13
+ t.before do
14
+ self.email = email.downcase
15
+ end
16
+
17
+ t.after do
18
+ if @foo
19
+ key[:foos].sadd(@foo)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def delete
26
+ super do |t|
27
+ foos = nil
28
+
29
+ t.before do
30
+ foos = key[:foos].smembers
31
+ end
32
+
33
+ t.after do
34
+ foos.each { |foo| key[:foos].srem(foo) }
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ test do
41
+ u = User.create(email: "FOO@BAR.COM", foo: "bar")
42
+ assert_equal "foo@bar.com", u.email
43
+ assert_equal ["bar"], u.key[:foos].smembers
44
+
45
+ u.delete
46
+ assert_equal [], User.key[u.id][:foos].smembers
47
+ end
48
+ end
data/test/filtering.rb ADDED
@@ -0,0 +1,42 @@
1
+ require File.expand_path("./helper", File.dirname(__FILE__))
2
+
3
+ class User < Ohm::Model
4
+ attribute :fname
5
+ attribute :lname
6
+ attribute :status
7
+ index :fname
8
+ index :lname
9
+ index :status
10
+ end
11
+
12
+ setup do
13
+ u1 = User.create(fname: "John", lname: "Doe", status: "active")
14
+ u2 = User.create(fname: "Jane", lname: "Doe", status: "active")
15
+
16
+ [u1, u2]
17
+ end
18
+
19
+ test "findability" do |john, jane|
20
+ assert_equal 1, User.find(lname: "Doe", fname: "John").size
21
+ assert User.find(lname: "Doe", fname: "John").include?(john)
22
+
23
+ assert_equal 1, User.find(lname: "Doe", fname: "Jane").size
24
+ assert User.find(lname: "Doe", fname: "Jane").include?(jane)
25
+ end
26
+
27
+ test "#first" do |john, jane|
28
+ set = User.find(lname: "Doe", status: "active")
29
+
30
+ assert_equal jane, set.first(by: "fname", order: "ALPHA")
31
+ assert_equal john, set.first(by: "fname", order: "ALPHA DESC")
32
+
33
+ assert_equal "Jane", set.first(by: "fname", order: "ALPHA", get: "fname")
34
+ assert_equal "John", set.first(by: "fname", order: "ALPHA DESC", get: "fname")
35
+ end
36
+
37
+ test "#[]" do |john, jane|
38
+ set = User.find(lname: "Doe", status: "active")
39
+
40
+ assert_equal john, set[john.id]
41
+ assert_equal jane, set[jane.id]
42
+ end
File without changes
data/test/indices.rb ADDED
@@ -0,0 +1,97 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ class User < Ohm::Model
6
+ attribute :email
7
+ attribute :update
8
+ attribute :activation_code
9
+ attribute :sandunga
10
+ index :email
11
+ index :email_provider
12
+ index :working_days
13
+ index :update
14
+ index :activation_code
15
+
16
+ def working_days
17
+ @working_days ||= []
18
+ end
19
+
20
+ def email_provider
21
+ email.split("@").last
22
+ end
23
+
24
+ def before_save
25
+ self.activation_code ||= "user:#{id}"
26
+ end
27
+ end
28
+
29
+ setup do
30
+ @user1 = User.create(email: "foo", activation_code: "bar", update: "baz")
31
+ @user2 = User.create(email: "bar")
32
+ @user3 = User.create(email: "baz qux")
33
+ end
34
+
35
+ test "be able to find by the given attribute" do
36
+ assert @user1 == User.find(email: "foo").first
37
+ end
38
+
39
+ test "raise an error if the parameter supplied is not a hash" do
40
+ begin
41
+ User.find(1)
42
+ rescue => ex
43
+ ensure
44
+ assert ex.kind_of?(ArgumentError)
45
+ assert ex.message == "You need to supply a hash with filters. If you want to find by ID, use User[id] instead."
46
+ end
47
+ end
48
+
49
+ test "avoid intersections with the all collection" do
50
+ assert_equal "User:indices:email:foo", User.find(email: "foo").key
51
+ end
52
+
53
+ test "cleanup the temporary key after use" do
54
+ assert User.find(:email => "foo", :activation_code => "bar").to_a
55
+
56
+ assert Ohm.redis.keys("User:temp:*").empty?
57
+ end
58
+
59
+ test "allow multiple chained finds" do
60
+ assert 1 == User.find(:email => "foo").find(:activation_code => "bar").find(:update => "baz").size
61
+ end
62
+
63
+ test "return nil if no results are found" do
64
+ assert User.find(:email => "foobar").empty?
65
+ assert nil == User.find(:email => "foobar").first
66
+ end
67
+
68
+ test "update indices when changing attribute values" do
69
+ @user1.email = "baz"
70
+ @user1.save
71
+
72
+ assert [] == User.find(:email => "foo").to_a
73
+ assert [@user1] == User.find(:email => "baz").to_a
74
+ end
75
+
76
+ test "remove from the index after deleting" do
77
+ @user2.delete
78
+
79
+ assert [] == User.find(:email => "bar").to_a
80
+ end
81
+
82
+ test "work with attributes that contain spaces" do
83
+ assert [@user3] == User.find(:email => "baz qux").to_a
84
+ end
85
+
86
+ # Indexing arbitrary attributes
87
+ setup do
88
+ @user1 = User.create(:email => "foo@gmail.com")
89
+ @user2 = User.create(:email => "bar@gmail.com")
90
+ @user3 = User.create(:email => "bazqux@yahoo.com")
91
+ end
92
+
93
+ test "allow indexing by an arbitrary attribute" do
94
+ gmail = User.find(:email_provider => "gmail.com").to_a
95
+ assert [@user1, @user2] == gmail.sort_by { |u| u.id }
96
+ assert [@user3] == User.find(:email_provider => "yahoo.com").to_a
97
+ end