related 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,4 +1,10 @@
1
1
 
2
+ *0.4*
3
+
4
+ * Related.root
5
+ * Custom relationship weights
6
+ * More compact key format
7
+
2
8
  *0.3*
3
9
 
4
10
  * ActiveModel support
data/README.md CHANGED
@@ -112,6 +112,26 @@ like this to get a random selection of nodes:
112
112
  node.outgoing(:friends).nodes.limit(5)
113
113
  ```
114
114
 
115
+ The root node
116
+ -------------
117
+
118
+ Related provides a special kind of node called the "root" node. It's always
119
+ accessible using the `Related.root` helper and you can create a relationship
120
+ between any node and the root node, which is useful if you want to easily
121
+ access a set of nodes without knowing the IDs of those nodes.
122
+
123
+ ```ruby
124
+ Related::Relationship.create(:example, Related.root, node)
125
+ Related.root.outgoing(:example)
126
+ ```
127
+
128
+ You can even add attributes to the root node if you want.
129
+
130
+ ```ruby
131
+ Related.root.name = 'The root'
132
+ Related.root.save
133
+ ```
134
+
115
135
  Properties
116
136
  ----------
117
137
 
@@ -161,6 +181,42 @@ node.outgoing(:attending).options(
161
181
  )
162
182
  ```
163
183
 
184
+ Weight
185
+ ------
186
+
187
+ All relationships have an associated weight on its incoming and outgoing
188
+ links. By default the weight is set to the time when the relationship was
189
+ created. That makes the result from a query that fetches relationships always
190
+ sorted so that newer relationships appear first, which is nice. If you create
191
+ a custom Related::Relationship sub-class you can define how the weight is
192
+ generated for a relationship.
193
+
194
+ ```ruby
195
+ class Comment < Related::Relationship
196
+ property :created_at, Time
197
+ property :points, Integer
198
+ weight do |direction|
199
+ if direction == :in
200
+ self.created_at
201
+ elsif direction == :out
202
+ self.points
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ The weight is always an integer and is sorted in descending order.
209
+
210
+ The weight for the links get updated every time the relationship is saved. So
211
+ if you update the points for a Comment in the example above, the weight is
212
+ automatically updated. You can access the weight and rank (0 based position)
213
+ of a relationship like this:
214
+
215
+ ```ruby
216
+ comment.weight(:out)
217
+ comment.rank(:in)
218
+ ```
219
+
164
220
  ActiveModel
165
221
  -----------
166
222
 
@@ -2,5 +2,12 @@ module Related
2
2
  class RelatedException < RuntimeError; end
3
3
  class NotFound < RelatedException; end
4
4
  class InvalidQuery < RelatedException; end
5
- class ValidationsFailed < RelatedException; end
5
+ class ValidationsFailed < RelatedException
6
+ attr_reader :object
7
+ def initialize(object)
8
+ @object = object
9
+ errors = @object.errors.full_messages.to_sentence
10
+ super(errors)
11
+ end
12
+ end
6
13
  end
@@ -11,5 +11,10 @@ module Related
11
11
  ).gsub('/','x').gsub('+','y').gsub('=','').strip
12
12
  end
13
13
 
14
+ # Returns the root node for the graph
15
+ def root
16
+ @root ||= Related::Root.new
17
+ end
18
+
14
19
  end
15
20
  end
data/lib/related/node.rb CHANGED
@@ -46,6 +46,7 @@ module Related
46
46
  def page(nr)
47
47
  query = self.query
48
48
  query.page = nr
49
+ query.result_type = :relationships
49
50
  query
50
51
  end
51
52
 
@@ -176,7 +177,7 @@ module Related
176
177
  @page && @page.to_i != 1 ? (@page.to_i * @limit.to_i) - @limit.to_i : 0
177
178
  else
178
179
  rel = @page.is_a?(String) ? Related::Relationship.find(@page) : @page
179
- rel.rank + 1
180
+ rel.rank(@direction) + 1
180
181
  end
181
182
  end
182
183
 
@@ -186,9 +187,9 @@ module Related
186
187
 
187
188
  def key(node=nil)
188
189
  if @result_type == :nodes
189
- "#{node ? node.to_s : @node.to_s}:nodes:#{@relationship_type}:#{@direction}"
190
+ "#{node ? node.to_s : @node.to_s}:n:#{@relationship_type}:#{@direction}"
190
191
  else
191
- "#{node ? node.to_s : @node.to_s}:rel:#{@relationship_type}:#{@direction}"
192
+ "#{node ? node.to_s : @node.to_s}:r:#{@relationship_type}:#{@direction}"
192
193
  end
193
194
  end
194
195
 
@@ -18,8 +18,16 @@ module Related
18
18
  @end_node ||= Related::Node.find(end_node_id)
19
19
  end
20
20
 
21
- def rank
22
- Related.redis.zrevrank("#{self.start_node_id}:rel:#{self.label}:out", self.id)
21
+ def rank(direction)
22
+ Related.redis.zrevrank(r_key(direction), self.id)
23
+ end
24
+
25
+ def weight(direction)
26
+ Related.redis.zscore(r_key(direction), self.id).to_i
27
+ end
28
+
29
+ def self.weight(&block)
30
+ @weight = block
23
31
  end
24
32
 
25
33
  def self.create(label, node1, node2, attributes = {})
@@ -32,26 +40,58 @@ module Related
32
40
 
33
41
  private
34
42
 
43
+ def r_key(direction)
44
+ if direction.to_sym == :out
45
+ "#{self.start_node_id}:r:#{self.label}:out"
46
+ elsif direction.to_sym == :in
47
+ "#{self.end_node_id}:r:#{self.label}:in"
48
+ end
49
+ end
50
+
51
+ def n_key(direction)
52
+ if direction.to_sym == :out
53
+ "#{self.start_node_id}:n:#{self.label}:out"
54
+ elsif direction.to_sym == :in
55
+ "#{self.end_node_id}:n:#{self.label}:in"
56
+ end
57
+ end
58
+
59
+ def self.weight_for(relationship, direction)
60
+ if @weight
61
+ relationship.instance_exec(direction, &@weight).to_i
62
+ else
63
+ Time.parse(relationship.created_at).to_i
64
+ end
65
+ end
66
+
35
67
  def create
36
68
  Related.redis.multi do
37
69
  super
38
- score = Time.now.to_i
39
- Related.redis.zadd("#{self.start_node_id}:rel:#{self.label}:out", score, self.id)
40
- Related.redis.zadd("#{self.end_node_id}:rel:#{self.label}:in", score, self.id)
70
+ Related.redis.zadd(r_key(:out), self.class.weight_for(self, :out), self.id)
71
+ Related.redis.zadd(r_key(:in), self.class.weight_for(self, :in), self.id)
72
+ Related.redis.sadd(n_key(:out), self.end_node_id)
73
+ Related.redis.sadd(n_key(:in), self.start_node_id)
74
+ end
75
+ self
76
+ end
41
77
 
42
- Related.redis.sadd("#{self.start_node_id}:nodes:#{self.label}:out", self.end_node_id)
43
- Related.redis.sadd("#{self.end_node_id}:nodes:#{self.label}:in", self.start_node_id)
78
+ def update
79
+ Related.redis.multi do
80
+ super
81
+ Related.redis.zadd(r_key(:out), self.class.weight_for(self, :out), self.id)
82
+ Related.redis.zadd(r_key(:in), self.class.weight_for(self, :in), self.id)
83
+ Related.redis.sadd(n_key(:out), self.end_node_id)
84
+ Related.redis.sadd(n_key(:in), self.start_node_id)
44
85
  end
45
86
  self
46
87
  end
47
88
 
48
89
  def delete
49
90
  Related.redis.multi do
50
- Related.redis.zrem("#{self.start_node_id}:rel:#{self.label}:out", self.id)
51
- Related.redis.zrem("#{self.end_node_id}:rel:#{self.label}:in", self.id)
52
-
53
- Related.redis.srem("#{self.start_node_id}:nodes:#{self.label}:out", self.end_node_id)
54
- Related.redis.srem("#{self.end_node_id}:nodes:#{self.label}:in", self.start_node_id)
91
+ Related.redis.zrem(r_key(:out), self.id)
92
+ Related.redis.zrem(r_key(:in), self.id)
93
+ Related.redis.srem(n_key(:out), self.end_node_id)
94
+ Related.redis.srem(n_key(:in), self.start_node_id)
55
95
  super
56
96
  end
57
97
  self
@@ -0,0 +1,10 @@
1
+ module Related
2
+ class Root < Related::Node
3
+
4
+ def initialize(attributes = {})
5
+ @id = 'root'
6
+ super(attributes)
7
+ end
8
+
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module Related
2
- Version = VERSION = '0.3.1'
2
+ Version = VERSION = '0.4.0'
3
3
  end
data/lib/related.rb CHANGED
@@ -8,6 +8,7 @@ require 'related/exceptions'
8
8
  require 'related/entity'
9
9
  require 'related/node'
10
10
  require 'related/relationship'
11
+ require 'related/root'
11
12
 
12
13
  module Related
13
14
  include Helpers
@@ -85,6 +85,11 @@ class ActiveModelTest < ActiveModel::TestCase
85
85
  assert_raises Related::ValidationsFailed do
86
86
  like.save
87
87
  end
88
+ begin
89
+ like.save
90
+ rescue Related::ValidationsFailed => e
91
+ assert_equal like, e.object
92
+ end
88
93
  like.how_much = 1.0
89
94
  assert_equal true, like.valid?
90
95
  assert_nothing_raised do
data/test/model_test.rb CHANGED
@@ -12,6 +12,12 @@ class ModelTest < ActiveModel::TestCase
12
12
  end
13
13
  end
14
14
 
15
+ class Like < Related::Relationship
16
+ weight do |dir|
17
+ dir == :in ? in_score : out_score
18
+ end
19
+ end
20
+
15
21
  def setup
16
22
  Related.redis.flushall
17
23
  end
@@ -46,4 +52,30 @@ class ModelTest < ActiveModel::TestCase
46
52
  assert_equal Event, nodes.first.class
47
53
  end
48
54
 
55
+ def test_custom_weight
56
+ node1 = Related::Node.create
57
+ node2 = Related::Node.create
58
+ like = Like.create(:like, node1, node2, :in_score => 42, :out_score => 10)
59
+ assert_equal 42, like.weight(:in)
60
+ assert_equal 10, like.weight(:out)
61
+ like.in_score = 50
62
+ like.save
63
+ assert_equal 50, like.weight(:in)
64
+ assert_equal 10, like.weight(:out)
65
+ end
66
+
67
+ def test_weight_sorting
68
+ node1 = Related::Node.create
69
+ node2 = Related::Node.create
70
+ node3 = Related::Node.create
71
+ rel1 = Like.create(:like, node1, node2, :in_score => 1, :out_score => 1)
72
+ rel2 = Like.create(:like, node1, node3, :in_score => 2, :out_score => 2)
73
+ rel3 = Like.create(:like, node2, node1, :in_score => 1, :out_score => 1)
74
+ rel4 = Like.create(:like, node3, node1, :in_score => 2, :out_score => 2)
75
+ assert_equal [rel2,rel1], node1.outgoing(:like).relationships.options(:model => lambda { Like }).to_a
76
+ assert_equal [rel4,rel3], node1.incoming(:like).relationships.options(:model => lambda { Like }).to_a
77
+ assert_equal [rel1], node1.outgoing(:like).relationships.options(:model => lambda { Like }).per_page(2).page(rel2).to_a
78
+ assert_equal [rel3], node1.incoming(:like).relationships.options(:model => lambda { Like }).per_page(2).page(rel4).to_a
79
+ end
80
+
49
81
  end
data/test/related_test.rb CHANGED
@@ -102,9 +102,9 @@ class RelatedTest < Test::Unit::TestCase
102
102
  def test_can_create_a_relationship_with_attributes
103
103
  node1 = Related::Node.create(:name => 'One')
104
104
  node2 = Related::Node.create(:name => 'Two')
105
- rel = Related::Relationship.create(:friends, node1, node2, :weight => 2.5)
105
+ rel = Related::Relationship.create(:friends, node1, node2, :score => 2.5)
106
106
  rel = Related::Relationship.find(rel.id)
107
- assert_equal '2.5', rel.weight
107
+ assert_equal '2.5', rel.score
108
108
  end
109
109
 
110
110
  def test_can_delete_a_relationship
@@ -141,6 +141,7 @@ class RelatedTest < Test::Unit::TestCase
141
141
  node3 = Related::Node.create
142
142
  node4 = Related::Node.create
143
143
  node5 = Related::Node.create
144
+ node6 = Related::Node.create
144
145
  rel1 = Related::Relationship.create(:friends, node1, node2, :name => 'rel1')
145
146
  sleep(1)
146
147
  rel2 = Related::Relationship.create(:friends, node1, node3, :name => 'rel2')
@@ -149,12 +150,12 @@ class RelatedTest < Test::Unit::TestCase
149
150
  sleep(1)
150
151
  rel4 = Related::Relationship.create(:friends, node1, node5, :name => 'rel4')
151
152
  sleep(1)
152
- rel5 = Related::Relationship.create(:friends, node1, node5, :name => 'rel5')
153
- assert_equal [rel5,rel4,rel3], node1.outgoing(:friends).relationships.per_page(3).page(1).to_a
154
- assert_equal [rel2,rel1], node1.outgoing(:friends).relationships.per_page(3).page(2).to_a
155
- assert_equal [rel5,rel4,rel3], node1.outgoing(:friends).relationships.per_page(3).page(nil).to_a
156
- assert_equal [rel4,rel3,rel2], node1.outgoing(:friends).relationships.per_page(3).page(rel5).to_a
157
- assert_equal [rel2,rel1], node1.outgoing(:friends).relationships.per_page(3).page(rel3).to_a
153
+ rel5 = Related::Relationship.create(:friends, node1, node6, :name => 'rel5')
154
+ assert_equal [rel5,rel4,rel3], node1.outgoing(:friends).per_page(3).page(1).to_a
155
+ assert_equal [rel2,rel1], node1.outgoing(:friends).per_page(3).page(2).to_a
156
+ assert_equal [rel5,rel4,rel3], node1.outgoing(:friends).per_page(3).page(nil).to_a
157
+ assert_equal [rel4,rel3,rel2], node1.outgoing(:friends).per_page(3).page(rel5).to_a
158
+ assert_equal [rel2,rel1], node1.outgoing(:friends).per_page(3).page(rel3).to_a
158
159
  end
159
160
 
160
161
  def test_can_count_the_number_of_related_nodes
@@ -285,4 +286,13 @@ class RelatedTest < Test::Unit::TestCase
285
286
  end
286
287
  end
287
288
 
289
+ def test_root
290
+ assert_kind_of Related::Node, Related.root
291
+ node = Related::Node.create
292
+ rel = Related::Relationship.create(:friend, Related.root, node)
293
+ assert_equal [node], Related.root.outgoing(:friend).to_a
294
+ Related.root.name = 'Test'
295
+ Related.root.save
296
+ end
297
+
288
298
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: related
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 1
10
- version: 0.3.1
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Niklas Holmgren
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-10 00:00:00 +02:00
18
+ date: 2011-10-11 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -84,6 +84,7 @@ files:
84
84
  - lib/related/helpers.rb
85
85
  - lib/related/node.rb
86
86
  - lib/related/relationship.rb
87
+ - lib/related/root.rb
87
88
  - lib/related/version.rb
88
89
  - lib/related.rb
89
90
  - test/active_model_test.rb