ohm 0.1.5 → 1.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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