related 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6 -0
- data/README.md +147 -50
- data/lib/related/entity.rb +225 -107
- data/lib/related/exceptions.rb +1 -0
- data/lib/related/follower.rb +8 -8
- data/lib/related/helpers.rb +3 -3
- data/lib/related/node.rb +18 -2
- data/lib/related/version.rb +1 -1
- data/lib/related.rb +1 -1
- data/test/active_model_test.rb +132 -0
- data/test/model_test.rb +49 -0
- data/test/related_test.rb +1 -9
- metadata +11 -11
data/CHANGELOG
CHANGED
data/README.md
CHANGED
@@ -11,7 +11,9 @@ use. The intention is not to compete with industrial grade graph databases
|
|
11
11
|
like Neo4j, but rather to be a replacement for a relational database when your
|
12
12
|
data is better described as a graph. For example when building social
|
13
13
|
software. Related is meant to be web scale, but ultimately relies on the
|
14
|
-
ability of Redis to scale (using Redis cluster for example).
|
14
|
+
ability of Redis to scale (using Redis cluster for example). Read more about
|
15
|
+
the philosophy behind Related in the
|
16
|
+
[Wiki](http://github.com/sutajio/related/wiki).
|
15
17
|
|
16
18
|
Setup
|
17
19
|
-----
|
@@ -22,8 +24,10 @@ Assuming you already have Redis installed:
|
|
22
24
|
|
23
25
|
Or add the gem to your Gemfile.
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
+
```ruby
|
28
|
+
require 'related'
|
29
|
+
Related.redis = 'redis://.../'
|
30
|
+
```
|
27
31
|
|
28
32
|
If you are using Rails, add the above to an initializer. If Redis is running
|
29
33
|
on localhost and on the default port the second line is not needed.
|
@@ -31,52 +35,69 @@ on localhost and on the default port the second line is not needed.
|
|
31
35
|
Example usage
|
32
36
|
-------------
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
38
|
+
```ruby
|
39
|
+
node = Related::Node.create(:name => 'Example', :popularity => 2.3)
|
40
|
+
node.new?
|
41
|
+
node.popularity = 100
|
42
|
+
node.attributes
|
43
|
+
node.has_attribute?(:popularity)
|
44
|
+
node.read_attribute(:popularity)
|
45
|
+
node.write_attribute(:popularity, 50)
|
46
|
+
node.save
|
47
|
+
node.persisted?
|
48
|
+
node = Related::Node.find(node.id)
|
49
|
+
node.destroy
|
50
|
+
node.destroyed?
|
51
|
+
|
52
|
+
node1 = Related::Node.create
|
53
|
+
node2 = Related::Node.create
|
54
|
+
rel = Related::Relationship.create(:friends, node1, node2, :have_met => true)
|
55
|
+
|
56
|
+
n = Related::Node.find(node1.id)
|
57
|
+
nn = Related::Node.find(node1.id, node2.id)
|
58
|
+
|
59
|
+
n = Related::Node.find(node1.id, :fields => [:name])
|
60
|
+
nn = Related::Node.find(node1.id, node2.id, :fields => [:name])
|
61
|
+
```
|
50
62
|
|
51
63
|
Nodes and relationships are both sub-classes of the same base class and both
|
52
64
|
behave similar to an ActiveRecord object and can store attributes etc.
|
53
65
|
|
54
66
|
To query the graph:
|
55
67
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
68
|
+
```ruby
|
69
|
+
node.outgoing(:friends)
|
70
|
+
node.incoming(:friends)
|
71
|
+
node.outgoing(:friends).relationships
|
72
|
+
node.outgoing(:friends).nodes
|
73
|
+
node.outgoing(:friends).limit(5)
|
74
|
+
node.outgoing(:friends).options(:fields => ..., :model => ...)
|
75
|
+
node1.path_to(node2).outgoing(:friends).depth(3)
|
76
|
+
node1.shortest_path_to(node2).outgoing(:friends).depth(3)
|
77
|
+
```
|
63
78
|
|
64
79
|
To get the results from a query:
|
65
80
|
|
66
|
-
|
67
|
-
|
81
|
+
```ruby
|
82
|
+
node.outgoing(:friends).to_a
|
83
|
+
node.outgoing(:friends).count (or .size, which is memoized)
|
84
|
+
```
|
68
85
|
|
69
86
|
You can also do set operations, like union, diff and intersect:
|
70
87
|
|
71
|
-
|
72
|
-
|
73
|
-
|
88
|
+
```ruby
|
89
|
+
node1.outgoing(:friends).union(node2.outgoing(:friends))
|
90
|
+
node1.outgoing(:friends).diff(node2.outgoing(:friends))
|
91
|
+
node1.outgoing(:friends).intersect(node2.outgoing(:friends))
|
92
|
+
```
|
74
93
|
|
75
94
|
Relationships are sorted based on when they were created, which means you can
|
76
95
|
paginate them:
|
77
96
|
|
78
|
-
|
79
|
-
|
97
|
+
```ruby
|
98
|
+
node.outgoing(:friends).relationships.per_page(100).page(1)
|
99
|
+
node.outgoing(:friends).relationships.per_page(100).page(rel)
|
100
|
+
```
|
80
101
|
|
81
102
|
The second form paginates based on the id of the last relationship on the
|
82
103
|
previous page. Useful for cases where explicit page numbers are not
|
@@ -87,7 +108,79 @@ without going through the extra step of iterating through the relationship
|
|
87
108
|
objects you will only get random nodes. Thus you can use .limit (or .per_page)
|
88
109
|
like this to get a random selection of nodes:
|
89
110
|
|
90
|
-
|
111
|
+
```ruby
|
112
|
+
node.outgoing(:friends).nodes.limit(5)
|
113
|
+
```
|
114
|
+
|
115
|
+
Properties
|
116
|
+
----------
|
117
|
+
|
118
|
+
All Node and Relationship attributes are stored as strings in Redis, but you
|
119
|
+
can easily create your own subclass and define your own custom serialization
|
120
|
+
behavior. You can either just override the getter and setter methods for the
|
121
|
+
attribute you need to convert or you can use the `property` method to define
|
122
|
+
the semantics and let Related do the conversion for you.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Event < Related::Node
|
126
|
+
property :title, String
|
127
|
+
property :attending_count, Integer
|
128
|
+
property :popularity, Float
|
129
|
+
property :start_date, DateTime
|
130
|
+
property :location do |value|
|
131
|
+
"http://maps.google.com/maps?q=#{value}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
An additional benefit of defining properties like this is that they get
|
137
|
+
included when you serialize the object to JSON or XML even when the attribute
|
138
|
+
hasn't been set.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
event = Event.create(:title => 'Party!', :location => 'Stockholm')
|
142
|
+
event.as_json # => {"title"=>"Party!","attending_count"=>nil,"popularity"=>nil,"start_date"=>nil,"location"=>"http://maps.google.com/maps?q=Stockholm"}
|
143
|
+
```
|
144
|
+
|
145
|
+
When querying the graph you may want the query to return the results as your
|
146
|
+
custom model class instead of as a Related::Node or Related::Relationship.
|
147
|
+
Related allows you to specify what model a specific node or relationship
|
148
|
+
should be instantiated as based on its attributes.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
Related::Node.find(...,
|
152
|
+
:model => lambda {|attributes|
|
153
|
+
attributes['start_date'] ? Event : Related::Node
|
154
|
+
}
|
155
|
+
)
|
156
|
+
|
157
|
+
node.outgoing(:attending).options(
|
158
|
+
:model => lambda {|attributes|
|
159
|
+
attributes['start_date'] ? Event : Related::Node
|
160
|
+
}
|
161
|
+
)
|
162
|
+
```
|
163
|
+
|
164
|
+
ActiveModel
|
165
|
+
-----------
|
166
|
+
|
167
|
+
Related supports ActiveModel and includes some basic functionality in both
|
168
|
+
nodes and relationships like validations, callbacks, JSON and XML
|
169
|
+
serialization and translation support. You can easily extend your own sub
|
170
|
+
classes with the custom ActiveModel functionality that you need.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class Like < Related::Relationship
|
174
|
+
validates_presence_of :how_much
|
175
|
+
validates_numericality_of :how_much
|
176
|
+
|
177
|
+
after_save :invalidate_cache
|
178
|
+
|
179
|
+
def invalidate_cache
|
180
|
+
...
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
91
184
|
|
92
185
|
Follower
|
93
186
|
--------
|
@@ -95,22 +188,26 @@ Follower
|
|
95
188
|
Related includes a helper module called Related::Follower that you can include
|
96
189
|
in your node sub-class to get basic Twitter-like follow functionality:
|
97
190
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
191
|
+
```ruby
|
192
|
+
require 'related/follower'
|
193
|
+
|
194
|
+
class User < Related::Node
|
195
|
+
include Related::Follower
|
196
|
+
end
|
197
|
+
|
198
|
+
user1 = User.create
|
199
|
+
user2 = User.create
|
200
|
+
|
201
|
+
user1.follow!(user2)
|
202
|
+
user1.unfollow!(user2)
|
203
|
+
user2.followers
|
204
|
+
user1.following
|
205
|
+
user1.friends
|
206
|
+
user2.followed_by?(user1)
|
207
|
+
user1.following?(user2)
|
208
|
+
user2.followers_count
|
209
|
+
user1.following_count
|
210
|
+
```
|
114
211
|
|
115
212
|
The two nodes does not need to be of the same type. You can for example have
|
116
213
|
a User following a Page or whatever makes sense in your app.
|
data/lib/related/entity.rb
CHANGED
@@ -1,134 +1,252 @@
|
|
1
1
|
module Related
|
2
2
|
class Entity
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@attributes[
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
3
|
+
extend ActiveModel::Naming
|
4
|
+
extend ActiveModel::Callbacks
|
5
|
+
include ActiveModel::Conversion
|
6
|
+
include ActiveModel::Validations
|
7
|
+
include ActiveModel::Serializers::JSON
|
8
|
+
include ActiveModel::Serializers::Xml
|
9
|
+
include ActiveModel::Translation
|
10
|
+
|
11
|
+
self.include_root_in_json = false
|
12
|
+
|
13
|
+
define_model_callbacks :create, :update, :destroy, :save
|
14
|
+
|
15
|
+
attr_reader :id
|
16
|
+
attr_reader :attributes
|
17
|
+
|
18
|
+
def initialize(attributes = {})
|
19
|
+
@attributes = {}
|
20
|
+
attributes.each do |key,value|
|
21
|
+
serializer = self.class.property_serializer(key)
|
22
|
+
@attributes[key.to_s] = serializer ?
|
23
|
+
serializer.to_string(value) : value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
self.id
|
29
|
+
end
|
30
|
+
|
31
|
+
def attributes
|
32
|
+
@attributes ||= {}
|
33
|
+
self.class.properties.inject({}) { |memo,key|
|
34
|
+
memo[key.to_s] = nil
|
35
|
+
memo
|
36
|
+
}.merge(@attributes.inject({}) { |memo,(k,v)|
|
37
|
+
memo[k.to_s] = v
|
38
|
+
memo
|
39
|
+
}.merge('id' => self.id))
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_attribute(name)
|
43
|
+
@attributes ||= {}
|
44
|
+
@attributes[name.to_s] || @attributes[name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_attribute(name, value)
|
48
|
+
@attributes ||= {}
|
49
|
+
@attributes[name.to_s] = value
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_attribute?(name)
|
53
|
+
@attributes ||= {}
|
54
|
+
@attributes.has_key?(name.to_s) ||
|
55
|
+
@attributes.has_key?(name) ||
|
56
|
+
@properties.has_key?(name.to_sym)
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(sym, *args, &block)
|
60
|
+
if sym.to_s =~ /=$/
|
61
|
+
name = sym.to_s[0..-2]
|
62
|
+
serializer = self.class.property_serializer(name)
|
63
|
+
write_attribute(name,
|
64
|
+
serializer ? serializer.to_string(args.first) :
|
65
|
+
args.first)
|
66
|
+
else
|
67
|
+
serializer = self.class.property_serializer(sym)
|
68
|
+
serializer ? serializer.from_string(read_attribute(sym)) :
|
69
|
+
read_attribute(sym)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(other)
|
74
|
+
other.is_a?(Related::Entity) && self.to_key == other.to_key
|
75
|
+
end
|
76
|
+
|
77
|
+
def new?
|
78
|
+
@id.nil? ? true : false
|
79
|
+
end
|
80
|
+
|
81
|
+
alias new_record? new?
|
82
|
+
|
83
|
+
def persisted?
|
84
|
+
!new?
|
85
|
+
end
|
86
|
+
|
87
|
+
def destroyed?
|
88
|
+
@destroyed
|
89
|
+
end
|
90
|
+
|
91
|
+
def save
|
92
|
+
create_or_update
|
93
|
+
end
|
94
|
+
|
95
|
+
def destroy
|
96
|
+
delete
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.create(attributes = {})
|
100
|
+
self.new(attributes).save
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.find(*args)
|
104
|
+
options = args.size > 1 && args.last.is_a?(Hash) ? args.pop : {}
|
105
|
+
args.size == 1 && args.first.is_a?(String) ?
|
106
|
+
find_one(args.first, options) :
|
107
|
+
find_many(args.flatten, options)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.property(name, klass=nil, &block)
|
111
|
+
@properties ||= {}
|
112
|
+
@properties[name.to_sym] = Serializer.new(klass, block)
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.properties
|
116
|
+
@properties ? @properties.keys : []
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def load_attributes(id, attributes)
|
122
|
+
@id = id
|
123
|
+
@attributes = attributes
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_or_update
|
128
|
+
run_callbacks :save do
|
129
|
+
new? ? create : update
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def create
|
134
|
+
run_callbacks :create do
|
135
|
+
raise Related::ValidationsFailed, self unless valid?
|
67
136
|
@id = Related.generate_id
|
68
|
-
@attributes
|
137
|
+
@attributes ||= {}
|
138
|
+
@attributes.merge!('created_at' => Time.now.utc.iso8601)
|
69
139
|
Related.redis.hmset(@id, *@attributes.to_a.flatten)
|
70
|
-
self
|
71
140
|
end
|
141
|
+
self
|
142
|
+
end
|
72
143
|
|
73
|
-
|
74
|
-
|
144
|
+
def update
|
145
|
+
run_callbacks :update do
|
146
|
+
raise Related::ValidationsFailed, self unless valid?
|
147
|
+
@attributes ||= {}
|
148
|
+
@attributes.merge!('updated_at' => Time.now.utc.iso8601)
|
75
149
|
Related.redis.hmset(@id, *@attributes.to_a.flatten)
|
76
|
-
self
|
77
150
|
end
|
151
|
+
self
|
152
|
+
end
|
78
153
|
|
79
|
-
|
154
|
+
def delete
|
155
|
+
run_callbacks :destroy do
|
80
156
|
Related.redis.del(id)
|
81
157
|
@destroyed = true
|
82
|
-
self
|
83
158
|
end
|
159
|
+
self
|
160
|
+
end
|
84
161
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
attributes
|
162
|
+
def self.find_fields(id, fields)
|
163
|
+
res = Related.redis.hmget(id.to_s, *fields)
|
164
|
+
if res
|
165
|
+
attributes = {}
|
166
|
+
res.each_with_index do |value, i|
|
167
|
+
attributes[fields[i]] = value
|
93
168
|
end
|
169
|
+
attributes
|
94
170
|
end
|
171
|
+
end
|
95
172
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
end
|
173
|
+
def self.find_one(id, options = {})
|
174
|
+
attributes = options[:fields] ?
|
175
|
+
find_fields(id, options[:fields]) :
|
176
|
+
Related.redis.hgetall(id.to_s)
|
177
|
+
if attributes.empty?
|
178
|
+
if Related.redis.exists(id) == false
|
179
|
+
raise Related::NotFound, id
|
104
180
|
end
|
105
|
-
self.new(id, attributes)
|
106
181
|
end
|
182
|
+
klass = options[:model] ? options[:model].call(attributes) : self
|
183
|
+
klass.new.send(:load_attributes, id, attributes)
|
184
|
+
end
|
107
185
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
if options[:fields]
|
112
|
-
Related.redis.hmget(id.to_s, *options[:fields])
|
113
|
-
else
|
114
|
-
Related.redis.hgetall(id.to_s)
|
115
|
-
end
|
116
|
-
}
|
117
|
-
end
|
118
|
-
objects = []
|
119
|
-
ids.each_with_index do |id,i|
|
186
|
+
def self.find_many(ids, options = {})
|
187
|
+
res = Related.redis.pipelined do
|
188
|
+
ids.each {|id|
|
120
189
|
if options[:fields]
|
121
|
-
|
122
|
-
res[i].each_with_index do |value, i|
|
123
|
-
attributes[options[:fields][i]] = value
|
124
|
-
end
|
125
|
-
objects << self.new(id, attributes)
|
190
|
+
Related.redis.hmget(id.to_s, *options[:fields])
|
126
191
|
else
|
127
|
-
|
192
|
+
Related.redis.hgetall(id.to_s)
|
193
|
+
end
|
194
|
+
}
|
195
|
+
end
|
196
|
+
objects = []
|
197
|
+
ids.each_with_index do |id,i|
|
198
|
+
if options[:fields]
|
199
|
+
attributes = {}
|
200
|
+
res[i].each_with_index do |value, i|
|
201
|
+
attributes[options[:fields][i]] = value
|
128
202
|
end
|
203
|
+
klass = options[:model] ? options[:model].call(attributes) : self
|
204
|
+
objects << klass.new.send(:load_attributes, id, attributes)
|
205
|
+
else
|
206
|
+
attributes = Hash[*res[i]]
|
207
|
+
klass = options[:model] ? options[:model].call(attributes) : self
|
208
|
+
objects << klass.new.send(:load_attributes, id, attributes)
|
129
209
|
end
|
130
|
-
objects
|
131
210
|
end
|
211
|
+
objects
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.property_serializer(property)
|
215
|
+
@properties ||= {}
|
216
|
+
@properties[property.to_sym]
|
217
|
+
end
|
218
|
+
|
219
|
+
class Serializer
|
220
|
+
def initialize(klass, block = nil)
|
221
|
+
@klass = klass
|
222
|
+
@block = block
|
223
|
+
end
|
224
|
+
|
225
|
+
def to_string(value)
|
226
|
+
case @klass.to_s
|
227
|
+
when 'DateTime', 'Time'
|
228
|
+
value.iso8601
|
229
|
+
else
|
230
|
+
value.to_s
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def from_string(value)
|
235
|
+
value = case @klass.to_s
|
236
|
+
when 'String'
|
237
|
+
value.to_s
|
238
|
+
when 'Integer'
|
239
|
+
value.to_i
|
240
|
+
when 'Float'
|
241
|
+
value.to_f
|
242
|
+
when 'DateTime', 'Time'
|
243
|
+
Time.parse(value)
|
244
|
+
else
|
245
|
+
value
|
246
|
+
end unless value.nil?
|
247
|
+
@block ? @block.call(value) : value
|
248
|
+
end
|
249
|
+
end
|
132
250
|
|
133
251
|
end
|
134
252
|
end
|
data/lib/related/exceptions.rb
CHANGED
data/lib/related/follower.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module Related
|
2
2
|
module Follower
|
3
|
-
def follow!(
|
4
|
-
Related::Relationship.create(:follow, self,
|
3
|
+
def follow!(other)
|
4
|
+
Related::Relationship.create(:follow, self, other)
|
5
5
|
end
|
6
6
|
|
7
|
-
def unfollow!(
|
7
|
+
def unfollow!(other)
|
8
8
|
self.following.relationships.each do |rel|
|
9
|
-
if rel.end_node_id ==
|
9
|
+
if rel.end_node_id == other.id
|
10
10
|
rel.destroy
|
11
11
|
end
|
12
12
|
end
|
@@ -24,12 +24,12 @@ module Related
|
|
24
24
|
self.followers.intersect(self.following)
|
25
25
|
end
|
26
26
|
|
27
|
-
def followed_by?(
|
28
|
-
self.followers.include?(
|
27
|
+
def followed_by?(other)
|
28
|
+
self.followers.include?(other)
|
29
29
|
end
|
30
30
|
|
31
|
-
def following?(
|
32
|
-
self.following.include?(
|
31
|
+
def following?(other)
|
32
|
+
self.following.include?(other)
|
33
33
|
end
|
34
34
|
|
35
35
|
def followers_count
|
data/lib/related/helpers.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'base64'
|
2
|
-
require 'digest/
|
2
|
+
require 'digest/md5'
|
3
3
|
|
4
4
|
module Related
|
5
5
|
module Helpers
|
@@ -7,8 +7,8 @@ module Related
|
|
7
7
|
# Generate a unique id
|
8
8
|
def generate_id
|
9
9
|
Base64.encode64(
|
10
|
-
Digest::
|
11
|
-
).gsub('/','x').gsub('+','y').gsub('=','').strip
|
10
|
+
Digest::MD5.digest("#{Time.now}-#{rand}")
|
11
|
+
).gsub('/','x').gsub('+','y').gsub('=','').strip
|
12
12
|
end
|
13
13
|
|
14
14
|
end
|
data/lib/related/node.rb
CHANGED
@@ -13,6 +13,12 @@ module Related
|
|
13
13
|
query
|
14
14
|
end
|
15
15
|
|
16
|
+
def options(opt)
|
17
|
+
query = self.query
|
18
|
+
query.options = opt
|
19
|
+
query
|
20
|
+
end
|
21
|
+
|
16
22
|
def outgoing(type)
|
17
23
|
query = self.query
|
18
24
|
query.relationship_type = type
|
@@ -86,11 +92,13 @@ module Related
|
|
86
92
|
attr_writer :include_start_node
|
87
93
|
attr_writer :destination
|
88
94
|
attr_writer :search_algorithm
|
95
|
+
attr_writer :options
|
89
96
|
|
90
97
|
def initialize(node)
|
91
98
|
@node = node
|
92
99
|
@result_type = :nodes
|
93
100
|
@depth = 4
|
101
|
+
@options = {}
|
94
102
|
end
|
95
103
|
|
96
104
|
def each(&block)
|
@@ -104,9 +112,9 @@ module Related
|
|
104
112
|
def to_a
|
105
113
|
perform_query unless @result
|
106
114
|
if @result_type == :nodes
|
107
|
-
Related::Node.find(@result)
|
115
|
+
Related::Node.find(@result, @options)
|
108
116
|
else
|
109
|
-
Related::Relationship.find(@result)
|
117
|
+
Related::Relationship.find(@result, @options)
|
110
118
|
end
|
111
119
|
end
|
112
120
|
|
@@ -153,6 +161,14 @@ module Related
|
|
153
161
|
self
|
154
162
|
end
|
155
163
|
|
164
|
+
def as_json(options = {})
|
165
|
+
self.to_a
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_json(options = {})
|
169
|
+
self.as_json.to_json(options)
|
170
|
+
end
|
171
|
+
|
156
172
|
protected
|
157
173
|
|
158
174
|
def page_start
|
data/lib/related/version.rb
CHANGED
data/lib/related.rb
CHANGED
@@ -0,0 +1,132 @@
|
|
1
|
+
require File.expand_path('test/test_helper')
|
2
|
+
|
3
|
+
class ActiveModelTest < ActiveModel::TestCase
|
4
|
+
include ActiveModel::Lint::Tests
|
5
|
+
|
6
|
+
class Like < Related::Relationship
|
7
|
+
validates_numericality_of :how_much, :allow_nil => true
|
8
|
+
|
9
|
+
before_save :before_save_callback
|
10
|
+
after_save :after_save_callback
|
11
|
+
before_create :before_create_callback
|
12
|
+
after_create :after_create_callback
|
13
|
+
before_update :before_update_callback
|
14
|
+
after_update :after_update_callback
|
15
|
+
before_destroy :before_destroy_callback
|
16
|
+
after_destroy :after_destroy_callback
|
17
|
+
|
18
|
+
attr_reader :before_save_was_called
|
19
|
+
attr_reader :after_save_was_called
|
20
|
+
attr_reader :before_create_was_called
|
21
|
+
attr_reader :after_create_was_called
|
22
|
+
attr_reader :before_update_was_called
|
23
|
+
attr_reader :after_update_was_called
|
24
|
+
attr_reader :before_destroy_was_called
|
25
|
+
attr_reader :after_destroy_was_called
|
26
|
+
|
27
|
+
def before_save_callback
|
28
|
+
@before_save_was_called = true
|
29
|
+
end
|
30
|
+
def after_save_callback
|
31
|
+
@after_save_was_called = true
|
32
|
+
end
|
33
|
+
def before_create_callback
|
34
|
+
@before_create_was_called = true
|
35
|
+
end
|
36
|
+
def after_create_callback
|
37
|
+
@after_create_was_called = true
|
38
|
+
end
|
39
|
+
def before_update_callback
|
40
|
+
@before_update_was_called = true
|
41
|
+
end
|
42
|
+
def after_update_callback
|
43
|
+
@after_update_was_called = true
|
44
|
+
end
|
45
|
+
def before_destroy_callback
|
46
|
+
@before_destroy_was_called = true
|
47
|
+
end
|
48
|
+
def after_destroy_callback
|
49
|
+
@after_destroy_was_called = true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup
|
54
|
+
Related.redis.flushall
|
55
|
+
@model = Related::Entity.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_attributes_has_id
|
59
|
+
node = Related::Entity.create
|
60
|
+
assert_equal node.id, node.attributes['id']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_can_return_json
|
64
|
+
node = Related::Entity.create(:name => 'test')
|
65
|
+
json = { :node => node }.to_json
|
66
|
+
json = JSON.parse(json)
|
67
|
+
assert_equal node.id, json['node']['id']
|
68
|
+
assert_equal node.name, json['node']['name']
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_query_can_return_json
|
72
|
+
node1 = Related::Node.create(:name => 'node1')
|
73
|
+
node2 = Related::Node.create(:name => 'node2')
|
74
|
+
Related::Relationship.create(:friends, node1, node2)
|
75
|
+
json = { :nodes => node1.outgoing(:friends) }.to_json
|
76
|
+
json = JSON.parse(json)
|
77
|
+
assert_equal node2.id, json['nodes'][0]['id']
|
78
|
+
assert_equal node2.name, json['nodes'][0]['name']
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_validations
|
82
|
+
like = Like.new(:how_much => 'not much')
|
83
|
+
assert_equal false, like.valid?
|
84
|
+
assert_equal [:how_much], like.errors.messages.keys
|
85
|
+
assert_raises Related::ValidationsFailed do
|
86
|
+
like.save
|
87
|
+
end
|
88
|
+
like.how_much = 1.0
|
89
|
+
assert_equal true, like.valid?
|
90
|
+
assert_nothing_raised do
|
91
|
+
like.save
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_save_callbacks
|
96
|
+
like = Like.new
|
97
|
+
assert_equal nil, like.before_save_was_called
|
98
|
+
assert_equal nil, like.after_save_was_called
|
99
|
+
like.save
|
100
|
+
assert_equal true, like.before_save_was_called
|
101
|
+
assert_equal true, like.after_save_was_called
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_create_callbacks
|
105
|
+
like = Like.new
|
106
|
+
assert_equal nil, like.before_create_was_called
|
107
|
+
assert_equal nil, like.after_create_was_called
|
108
|
+
like.save
|
109
|
+
assert_equal true, like.before_create_was_called
|
110
|
+
assert_equal true, like.after_create_was_called
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_update_callbacks
|
114
|
+
like = Like.new
|
115
|
+
like.save
|
116
|
+
assert_equal nil, like.before_update_was_called
|
117
|
+
assert_equal nil, like.after_update_was_called
|
118
|
+
like.save
|
119
|
+
assert_equal true, like.before_update_was_called
|
120
|
+
assert_equal true, like.after_update_was_called
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_create_callbacks
|
124
|
+
like = Like.new
|
125
|
+
assert_equal nil, like.before_destroy_was_called
|
126
|
+
assert_equal nil, like.after_destroy_was_called
|
127
|
+
like.destroy
|
128
|
+
assert_equal true, like.before_destroy_was_called
|
129
|
+
assert_equal true, like.after_destroy_was_called
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
data/test/model_test.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path('test/test_helper')
|
2
|
+
|
3
|
+
class ModelTest < ActiveModel::TestCase
|
4
|
+
|
5
|
+
class Event < Related::Node
|
6
|
+
property :attending_count, Integer
|
7
|
+
property :popularity, Float
|
8
|
+
property :start_date, DateTime
|
9
|
+
property :end_date, DateTime
|
10
|
+
property :location do |value|
|
11
|
+
"http://maps.google.com/maps?q=#{value}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup
|
16
|
+
Related.redis.flushall
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_property_conversion
|
20
|
+
event = Event.create(
|
21
|
+
:attending_count => 42,
|
22
|
+
:popularity => 0.9,
|
23
|
+
:start_date => Time.parse('2011-01-01'),
|
24
|
+
:location => 'Stockholm')
|
25
|
+
event = Event.find(event.id)
|
26
|
+
assert_equal 42, event.attending_count
|
27
|
+
assert_equal 0.9, event.popularity
|
28
|
+
assert_equal Time.parse('2011-01-01'), event.start_date
|
29
|
+
assert_equal Time.parse('2011-01-01').iso8601, event.read_attribute(:start_date)
|
30
|
+
assert_equal nil, event.end_date
|
31
|
+
assert_equal "http://maps.google.com/maps?q=Stockholm", event.location
|
32
|
+
assert event.as_json.has_key?('end_date')
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_model_factory
|
36
|
+
e1 = Event.create(:popularity => 0.0)
|
37
|
+
e2 = Event.create(:popularity => 1.0)
|
38
|
+
e1, e2 = Related::Node.find(e1.id, e2.id, :model =>
|
39
|
+
lambda {|attributes| attributes['popularity'].to_f > 0.5 ? Event : Related::Node })
|
40
|
+
assert_equal Related::Node, e1.class
|
41
|
+
assert_equal Event, e2.class
|
42
|
+
|
43
|
+
Related::Relationship.create(:test, e1, e2)
|
44
|
+
nodes = e1.outgoing(:test).nodes.options(:model =>
|
45
|
+
lambda {|attributes| attributes['popularity'].to_f > 0.5 ? Event : Related::Node }).to_a
|
46
|
+
assert_equal Event, nodes.first.class
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/test/related_test.rb
CHANGED
@@ -56,7 +56,7 @@ class RelatedTest < Test::Unit::TestCase
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def test_two_nodes_with_the_same_id_should_be_equal
|
59
|
-
assert_equal Related::Node.new('test', {}), Related::Node.new('test', {})
|
59
|
+
assert_equal Related::Node.new.send(:load_attributes, 'test', {}), Related::Node.new.send(:load_attributes, 'test', {})
|
60
60
|
end
|
61
61
|
|
62
62
|
def test_can_find_a_node_and_only_load_specific_attributes
|
@@ -265,14 +265,6 @@ class RelatedTest < Test::Unit::TestCase
|
|
265
265
|
assert_equal [node1], node2.incoming(:friends).intersect(node3.incoming(:friends)).to_a
|
266
266
|
end
|
267
267
|
|
268
|
-
def test_can_return_json
|
269
|
-
node = Related::Node.create(:name => 'test')
|
270
|
-
json = { :node => node }.to_json
|
271
|
-
json = JSON.parse(json)
|
272
|
-
assert_equal node.id, json['node']['id']
|
273
|
-
assert_equal node.name, json['node']['name']
|
274
|
-
end
|
275
|
-
|
276
268
|
def test_timestamps
|
277
269
|
node = Related::Node.create.save
|
278
270
|
node = Related::Node.find(node.id)
|
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:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.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-
|
18
|
+
date: 2011-10-07 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -51,19 +51,17 @@ dependencies:
|
|
51
51
|
type: :runtime
|
52
52
|
version_requirements: *id002
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
54
|
+
name: activemodel
|
55
55
|
prerelease: false
|
56
56
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
57
|
none: false
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
hash:
|
61
|
+
hash: 3
|
62
62
|
segments:
|
63
|
-
- 1
|
64
|
-
- 0
|
65
63
|
- 0
|
66
|
-
version:
|
64
|
+
version: "0"
|
67
65
|
type: :runtime
|
68
66
|
version_requirements: *id003
|
69
67
|
description: Related is a Redis-backed high performance graph database.
|
@@ -88,7 +86,9 @@ files:
|
|
88
86
|
- lib/related/relationship.rb
|
89
87
|
- lib/related/version.rb
|
90
88
|
- lib/related.rb
|
89
|
+
- test/active_model_test.rb
|
91
90
|
- test/follower_test.rb
|
91
|
+
- test/model_test.rb
|
92
92
|
- test/performance_test.rb
|
93
93
|
- test/redis-test.conf
|
94
94
|
- test/related_test.rb
|