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 CHANGED
@@ -1,4 +1,10 @@
1
1
 
2
+ *0.3*
3
+
4
+ * ActiveModel support
5
+ * Properties
6
+ * Dynamic sub-class loading
7
+
2
8
  *0.2*
3
9
 
4
10
  * Implemented set operations.
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
- require 'related'
26
- Related.redis = 'redis://.../'
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
- node = Related::Node.create(:name => 'Example', :popularity => 2.3)
35
- node.new_record?
36
- node.popularity = 100
37
- node.save
38
- node = Related::Node.find(node.id)
39
- node.destroy
40
-
41
- node1 = Related::Node.create
42
- node2 = Related::Node.create
43
- rel = Related::Relationship.create(:friends, node1, node2, :have_met => true)
44
-
45
- n = Related::Node.find(node1.id)
46
- nn = Related::Node.find(node1.id, node2.id)
47
-
48
- n = Related::Node.find(node1.id, :fields => [:name])
49
- nn = Related::Node.find(node1.id, node2.id, :fields => [:name])
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
- node.outgoing(:friends)
57
- node.incoming(:friends)
58
- node.outgoing(:friends).relationships
59
- node.outgoing(:friends).nodes
60
- node.outgoing(:friends).limit(5)
61
- node1.path_to(node2).outgoing(:friends).depth(3)
62
- node1.shortest_path_to(node2).outgoing(:friends).depth(3)
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
- node.outgoing(:friends).to_a
67
- node.outgoing(:friends).count (or .size, which is memoized)
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
- node1.outgoing(:friends).union(node2.outgoing(:friends))
72
- node1.outgoing(:friends).diff(node2.outgoing(:friends))
73
- node1.outgoing(:friends).intersect(node2.outgoing(:friends))
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
- node.outgoing(:friends).relationship.per_page(100).page(1)
79
- node.outgoing(:friends).relationship.per_page(100).page(rel)
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
- node.outgoing(:friends).nodes.limit(5)
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
- class User < Related::Node
99
- include Related::Follower
100
- end
101
-
102
- user1 = User.create
103
- user2 = User.create
104
-
105
- user1.follow!(user2)
106
- user1.unfollow!(user2)
107
- user2.followers
108
- user1.following
109
- user1.friends
110
- user2.followed_by?(user1)
111
- user1.following?(user2)
112
- user2.followers_count
113
- user1.following_count
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.
@@ -1,134 +1,252 @@
1
1
  module Related
2
2
  class Entity
3
-
4
- attr_reader :id
5
- attr_reader :attributes
6
- attr_reader :destroyed
7
-
8
- def initialize(*attributes)
9
- if attributes.first.is_a?(String)
10
- @id = attributes.first
11
- @attributes = attributes.last
12
- else
13
- @attributes = attributes.first
14
- end
15
- end
16
-
17
- def to_s
18
- self.id
19
- end
20
-
21
- def method_missing(sym, *args, &block)
22
- @attributes[sym] || @attributes[sym.to_s]
23
- end
24
-
25
- def ==(other)
26
- @id == other.id
27
- end
28
-
29
- def new_record?
30
- @id.nil? ? true : false
31
- end
32
-
33
- def save
34
- create_or_update
35
- end
36
-
37
- def destroy
38
- delete
39
- end
40
-
41
- def self.create(attributes = {})
42
- self.new(attributes).save
43
- end
44
-
45
- def self.find(*args)
46
- options = args.size > 1 && args.last.is_a?(Hash) ? args.pop : {}
47
- args.size == 1 && args.first.is_a?(String) ?
48
- find_one(args.first, options) :
49
- find_many(args.flatten, options)
50
- end
51
-
52
- def as_json(options = {})
53
- (attributes || {}).merge(:id => self.id)
54
- end
55
-
56
- def to_json(options = {})
57
- as_json.to_json(options)
58
- end
59
-
60
- private
61
-
62
- def create_or_update
63
- new_record? ? create : update
64
- end
65
-
66
- def create
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.merge!(:created_at => Time.now.utc.iso8601)
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
- def update
74
- @attributes.merge!(:updated_at => Time.now.utc.iso8601)
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
- def delete
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
- def self.find_fields(id, fields)
86
- res = Related.redis.hmget(id.to_s, *fields)
87
- if res
88
- attributes = {}
89
- res.each_with_index do |value, i|
90
- attributes[fields[i]] = value
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
- def self.find_one(id, options = {})
97
- attributes = options[:fields] ?
98
- find_fields(id, options[:fields]) :
99
- Related.redis.hgetall(id.to_s)
100
- if attributes.empty?
101
- if Related.redis.exists(id) == false
102
- raise Related::NotFound, id
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
- def self.find_many(ids, options = {})
109
- res = Related.redis.pipelined do
110
- ids.each {|id|
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
- attributes = {}
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
- objects << self.new(id, Hash[*res[i]])
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
@@ -2,4 +2,5 @@ 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
6
  end
@@ -1,12 +1,12 @@
1
1
  module Related
2
2
  module Follower
3
- def follow!(user)
4
- Related::Relationship.create(:follow, self, user)
3
+ def follow!(other)
4
+ Related::Relationship.create(:follow, self, other)
5
5
  end
6
6
 
7
- def unfollow!(user)
7
+ def unfollow!(other)
8
8
  self.following.relationships.each do |rel|
9
- if rel.end_node_id == user.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?(user)
28
- self.followers.include?(user)
27
+ def followed_by?(other)
28
+ self.followers.include?(other)
29
29
  end
30
30
 
31
- def following?(user)
32
- self.following.include?(user)
31
+ def following?(other)
32
+ self.following.include?(other)
33
33
  end
34
34
 
35
35
  def followers_count
@@ -1,5 +1,5 @@
1
1
  require 'base64'
2
- require 'digest/sha2'
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::SHA256.digest("#{Time.now}-#{rand}")
11
- ).gsub('/','x').gsub('+','y').gsub('=','').strip[0..21]
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
@@ -1,3 +1,3 @@
1
1
  module Related
2
- Version = VERSION = '0.2.1'
2
+ Version = VERSION = '0.3.0'
3
3
  end
data/lib/related.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'redis'
2
2
  require 'redis/namespace'
3
- require 'json'
3
+ require 'active_model'
4
4
 
5
5
  require 'related/version'
6
6
  require 'related/helpers'
@@ -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
@@ -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: 21
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 1
10
- version: 0.2.1
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-09-23 00:00:00 +02:00
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: json
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: 23
61
+ hash: 3
62
62
  segments:
63
- - 1
64
- - 0
65
63
  - 0
66
- version: 1.0.0
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