related 0.2.1 → 0.3.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.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