related 0.3.1 → 0.4.0

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/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