friendable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.simplecov +3 -0
- data/.travis.yml +12 -0
- data/Gemfile +7 -0
- data/LICENSE.md +22 -0
- data/README.md +118 -0
- data/Rakefile +2 -0
- data/friendable.gemspec +26 -0
- data/lib/friendable.rb +41 -0
- data/lib/friendable/exceptions.rb +6 -0
- data/lib/friendable/friendship.rb +70 -0
- data/lib/friendable/user_methods.rb +153 -0
- data/lib/friendable/version.rb +3 -0
- data/spec/friendable/friendship_spec.rb +114 -0
- data/spec/friendable/user_methods_spec.rb +153 -0
- data/spec/friendable_spec.rb +42 -0
- data/spec/spec_helper.rb +39 -0
- metadata +180 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3-p194@friendable --create
|
data/.simplecov
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (c) 2012 Yuki Nishijima
|
2
|
+
|
3
|
+
## MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# Friendable
|
2
|
+
|
3
|
+
You don't want to implement friendship functionality with RDB any more? But you think it's still too early to use graphDB? Use Redis!
|
4
|
+
|
5
|
+
[](http://travis-ci.org/yuki24/friendable) [](https://codeclimate.com/github/yuki24/friendable)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'friendable'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Next, include `Friendable::UserMethods` inside User class or something like that you use in your application:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class User < ActiveRecord::Base
|
21
|
+
include Friendable::UserMethods
|
22
|
+
|
23
|
+
...
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
Create a config file like below to set up Redis connection and save it as `config/initializers/friendable.rb`:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
Friendable.redis = Redis.new(host: "localhost", port: 6379)
|
31
|
+
```
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
### Being a friend
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
user = User.first
|
39
|
+
another_user = User.last
|
40
|
+
|
41
|
+
user.friends # => empty ActiveRecord::Relation
|
42
|
+
user.friend?(another_user) # => false
|
43
|
+
|
44
|
+
# they are now friends!
|
45
|
+
current_user.friend!(another_user)
|
46
|
+
|
47
|
+
user.friend?(another_user) # => true
|
48
|
+
user.friends.include?(another_user) # => true
|
49
|
+
another_user.friend?(user) # => true
|
50
|
+
another_user.friends.include?(user) # => true
|
51
|
+
```
|
52
|
+
|
53
|
+
You can also pass properties about their relationship. For example, if you want to store where they meet, you can do something like this:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
friendship = user.friend!(another_user, :meeting_place => "Red Rock")
|
57
|
+
|
58
|
+
friendship.meeting_place # => "Red Rock"
|
59
|
+
friendship.created_at # => "2012-09-23 14:24:03"
|
60
|
+
friendship.updated_at # => "2012-09-23 14:24:03"
|
61
|
+
```
|
62
|
+
|
63
|
+
If you want to find the specific friendship with somebody and edit/save the friendship, you can do it by using `#write_attribute` and `#save`:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
friendship = user.friendship_with(another_user)
|
67
|
+
friendship.write_attribute(:close_friend, true)
|
68
|
+
friendship.save
|
69
|
+
|
70
|
+
friendship.close_friend # => true
|
71
|
+
friendship.meeting_place # => "Red Rock"
|
72
|
+
friendship.created_at # => "2012-09-23 14:24:03"
|
73
|
+
friendship.updated_at # => "2012-09-23 14:25:21"
|
74
|
+
```
|
75
|
+
|
76
|
+
**Note:** properties are not saved on the opposite direction of the friendship.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
user.friend!(another_user, :meeting_place => "Red Rock")
|
80
|
+
|
81
|
+
friendship = user.friendship_with(another_user)
|
82
|
+
friendship.meeting_place # => "Red Rock"
|
83
|
+
|
84
|
+
friendship = another_user.friendship_with(user) # opposite
|
85
|
+
friendship.meeting_place # => NoMethodError
|
86
|
+
```
|
87
|
+
|
88
|
+
### Removing a friend
|
89
|
+
|
90
|
+
You can remove a user from the friend list of a user like this:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
user.unfriend!(another_user)
|
94
|
+
|
95
|
+
user.friend?(another_user) # => false
|
96
|
+
user.friends.include?(another_user) # => false
|
97
|
+
user.friend_ids.include?(another_user.id) # => false
|
98
|
+
```
|
99
|
+
|
100
|
+
For more details, have a look at the documentation.
|
101
|
+
|
102
|
+
## Support
|
103
|
+
|
104
|
+
* Ruby version: 1.9.3, 1.9.2, 1.8.7 and REE
|
105
|
+
* ORM: ActiveRecord
|
106
|
+
|
107
|
+
You also(and of course) need to install Redis.
|
108
|
+
|
109
|
+
## Contributing
|
110
|
+
|
111
|
+
1. Fork it
|
112
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
113
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
114
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
115
|
+
5. Create new Pull Request
|
116
|
+
|
117
|
+
## Copyright
|
118
|
+
Copyright (c) 2012 Yuki Nishijima. See LICENSE.md for further details.
|
data/Rakefile
ADDED
data/friendable.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/friendable/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Yuki Nishijima"]
|
6
|
+
gem.email = ["mail@yukinishijima.net"]
|
7
|
+
gem.description = %q{Redis backed friendship engine for your Ruby models}
|
8
|
+
gem.summary = %q{Redis backed friendship engine for your Ruby models}
|
9
|
+
gem.homepage = "https://github.com/yuki24/friendable"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "friendable"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Friendable::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'msgpack'
|
19
|
+
gem.add_dependency 'activesupport', '>= 3.0.0'
|
20
|
+
gem.add_dependency 'redis-namespace'
|
21
|
+
gem.add_dependency 'keytar'
|
22
|
+
|
23
|
+
gem.add_development_dependency 'rspec'
|
24
|
+
gem.add_development_dependency 'activerecord'
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
26
|
+
end
|
data/lib/friendable.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "friendable/version"
|
2
|
+
require "friendable/user_methods"
|
3
|
+
require "friendable/friendship"
|
4
|
+
require "friendable/exceptions"
|
5
|
+
require "redis-namespace"
|
6
|
+
|
7
|
+
module Friendable
|
8
|
+
extend self
|
9
|
+
attr_accessor :resource_class
|
10
|
+
|
11
|
+
def redis
|
12
|
+
@redis ||= Redis::Namespace.new(:friendable, :redis => Redis.new)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Accepts:
|
16
|
+
# 1. A 'hostname:port' String
|
17
|
+
# 2. A 'hostname:port:db' String (to select the Redis db)
|
18
|
+
# 3. A 'hostname:port/namespace' String (to set the Redis namespace)
|
19
|
+
# 4. A Redis URL String 'redis://host:port'
|
20
|
+
# 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
|
21
|
+
# or `Redis::Namespace`.
|
22
|
+
def redis=(server)
|
23
|
+
case server
|
24
|
+
when String
|
25
|
+
if server =~ /redis\:\/\//
|
26
|
+
redis = Redis.connect(:url => server, :thread_safe => true)
|
27
|
+
else
|
28
|
+
server, namespace = server.split('/', 2)
|
29
|
+
host, port, db = server.split(':')
|
30
|
+
redis = Redis.new(:host => host, :port => port, :thread_safe => true, :db => db)
|
31
|
+
end
|
32
|
+
namespace ||= :friendable
|
33
|
+
|
34
|
+
@redis = Redis::Namespace.new(namespace, :redis => redis)
|
35
|
+
when Redis::Namespace
|
36
|
+
@redis = server
|
37
|
+
else
|
38
|
+
@redis = Redis::Namespace.new(:friendable, :redis => server)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module Friendable
|
4
|
+
class Friendship
|
5
|
+
attr_accessor :target_resource, :source_resource
|
6
|
+
alias :friend :target_resource
|
7
|
+
|
8
|
+
def initialize(source_resource, target_resource, attrs = {})
|
9
|
+
# options = attrs.delete(:options) if attrs[:options]
|
10
|
+
@source_resource = source_resource
|
11
|
+
@target_resource = target_resource
|
12
|
+
@attributes = attrs.reverse_merge!(:created_at => nil, :updated_at => nil).symbolize_keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def write_attribute(attr_name, value)
|
16
|
+
@attributes[attr_name.to_sym] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def save
|
20
|
+
write_attribute(:created_at, Time.zone.now) unless @attributes[:created_at]
|
21
|
+
write_attribute(:updated_at, Time.zone.now)
|
22
|
+
|
23
|
+
Friendable.redis.hset(redis_key, target_resource.id, self.to_msgpack)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_msgpack
|
27
|
+
serializable.to_msgpack
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get a pretty string representation of the friendship, including the
|
31
|
+
# user who is a friend with and attributes for inspection.
|
32
|
+
#
|
33
|
+
def inspect
|
34
|
+
"#<Friendable::Friendship with: #{@target_resource.class}(id: #{@target_resource.id}), " <<
|
35
|
+
"attributes: " <<
|
36
|
+
@attributes.to_a.map{|ary| ary.join(": ") }.join(", ") << ">"
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.deserialize!(source_resource, target_resource, options)
|
40
|
+
attrs = MessagePack.unpack(options).symbolize_keys
|
41
|
+
attrs[:created_at] = Time.zone.at(attrs[:created_at])
|
42
|
+
attrs[:updated_at] = Time.zone.at(attrs[:updated_at])
|
43
|
+
|
44
|
+
Friendship.new(source_resource, target_resource, attrs)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def method_missing(method, *args, &block)
|
50
|
+
if method.to_s.last == "=" && @attributes.has_key?(method.to_s[0..-2].to_sym)
|
51
|
+
write_attribute(method.to_s[0..-2], args.first)
|
52
|
+
elsif @attributes.has_key?(method)
|
53
|
+
return @attributes[method]
|
54
|
+
else
|
55
|
+
super(method, args, block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def redis_key
|
60
|
+
source_resource.friend_list_key
|
61
|
+
end
|
62
|
+
|
63
|
+
def serializable
|
64
|
+
@attributes.clone.tap do |serializable_object|
|
65
|
+
serializable_object[:created_at] = serializable_object[:created_at].try(:utc).try(:to_i)
|
66
|
+
serializable_object[:updated_at] = serializable_object[:updated_at].try(:utc).try(:to_i)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'keytar'
|
3
|
+
|
4
|
+
module Friendable
|
5
|
+
module UserMethods
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
Friendable.resource_class = self
|
10
|
+
include Keytar
|
11
|
+
define_key :friend_list, :key_case => nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns an array object of the friend ids of your friends.
|
15
|
+
#
|
16
|
+
# ==== Examples
|
17
|
+
# current_user.friend!(target_user)
|
18
|
+
#
|
19
|
+
# current_user.friend_ids.include?(target_user.id) # => true
|
20
|
+
#
|
21
|
+
def friend_ids
|
22
|
+
@_friend_ids ||= (@_raw_friend_hashes ? @_raw_friend_hashes.keys : raw_friend_ids).map(&:to_i)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns an collection of user ressources(ActiveRecord::Relation).
|
26
|
+
#
|
27
|
+
# ==== Examples
|
28
|
+
# current_user.friends # => [#<User id: 1, ...>, #<User id: 2, ...>, ...]
|
29
|
+
# current_user.friends.first # => #<User id: 1, ...>
|
30
|
+
# current_user.friends.class # => ActiveRecord::Relation
|
31
|
+
#
|
32
|
+
def friends
|
33
|
+
@_friends ||= Friendable.resource_class.where(:id => friend_ids)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns an array of friendship objects. Currently this method does not support
|
37
|
+
# pagination.
|
38
|
+
#
|
39
|
+
# ==== Examples
|
40
|
+
# current_user.friendships # => [#<Friendable::Friendship>, ...]
|
41
|
+
# current_user.friendships.first # => #<Friendable::Friendship>
|
42
|
+
# current_user.friendships.class # => Array
|
43
|
+
#
|
44
|
+
def friendships
|
45
|
+
@_friendships ||= Friendable.resource_class.find(raw_friend_hashes.keys).map do |resource|
|
46
|
+
Friendship.deserialize!(self, resource, raw_friend_hashes[resource.id.to_s])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a friendship object that indicates the relation from a user to another user.
|
51
|
+
#
|
52
|
+
# ==== Examples
|
53
|
+
# current_user.friend!(target_user, :foo => "bar")
|
54
|
+
#
|
55
|
+
# friendship = current_user.friendship_with(target_user)
|
56
|
+
# friendship.friend == target_user # => true
|
57
|
+
# friendship.foo # => "bar"
|
58
|
+
# friendship.bar # => NoMethodError
|
59
|
+
#
|
60
|
+
def friendship_with(target_resource)
|
61
|
+
raw_friendship = @_raw_friend_hashes.try(:[], target_resource.id.to_s) || redis.hget(friend_list_key, target_resource.id)
|
62
|
+
|
63
|
+
if raw_friendship
|
64
|
+
Friendship.deserialize!(self, target_resource, raw_friendship)
|
65
|
+
else
|
66
|
+
raise Friendable::FriendshipNotFound, "user:#{self.id} is not a friend of the user:#{target_resource.id}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the number of the user's friends.
|
71
|
+
#
|
72
|
+
# ==== Examples
|
73
|
+
# current_user.friends_count # => 0
|
74
|
+
#
|
75
|
+
# current_user.friend!(target_user)
|
76
|
+
# current_user.friends_count # => 1
|
77
|
+
#
|
78
|
+
def friends_count
|
79
|
+
@_raw_friend_ids.try(:count) || @_raw_friend_hashes.try(:count) || redis.hlen(friend_list_key)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns if the given user is friend of you or not.
|
83
|
+
#
|
84
|
+
# ==== Examlpes
|
85
|
+
# current_user.friend?(target_user) # => false
|
86
|
+
#
|
87
|
+
# current_user.friend!(target_user)
|
88
|
+
#
|
89
|
+
# current_user.friend?(target_user) # => true
|
90
|
+
#
|
91
|
+
def friend?(resource)
|
92
|
+
@_raw_friend_ids.try(:include?, resource.id.to_s) || @_raw_friend_hashes.try(:has_key?, resource.id.to_s) || redis.hexists(friend_list_key, resource.id)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Adds the given user to your friend list. If the given user is already
|
96
|
+
# a friend of you and different options are given, the existing options
|
97
|
+
# will be replaced with the new options. It returns an object of
|
98
|
+
# Friendable::Friendship.
|
99
|
+
#
|
100
|
+
# ==== Examples
|
101
|
+
# current_user.friends.include?(target_user) # => false
|
102
|
+
# current_user.friend!(target_user) # => #<Friendable::Friendship>
|
103
|
+
# current_user.friends.include?(target_user) # => true
|
104
|
+
#
|
105
|
+
# friendship = current_user.friend!(target_user, :foo => "bar")
|
106
|
+
# friendship.foo # => "bar"
|
107
|
+
# friendship.bar # => NoMethodError
|
108
|
+
#
|
109
|
+
def friend!(resource, options = {})
|
110
|
+
# TODO: in the near future, I want to change to something like this:
|
111
|
+
# redis.multi do
|
112
|
+
# Friendship.new(self, resource, options).save(inverse: true)
|
113
|
+
# end
|
114
|
+
Friendship.new(self, resource, options).tap do |friendship|
|
115
|
+
redis.multi do
|
116
|
+
friendship.save
|
117
|
+
Friendship.new(resource, self).save
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Removes the given user from your friend list. If the given user is not
|
123
|
+
# a friend of you, it doesn't affect anything.
|
124
|
+
#
|
125
|
+
# ==== Examples
|
126
|
+
# current_user.friend!(target_user)
|
127
|
+
# current_user.friends.include?(target_user) # => true
|
128
|
+
#
|
129
|
+
# current_user.unfriend!(target_user)
|
130
|
+
# current_user.friends.include?(target_user) # => false
|
131
|
+
#
|
132
|
+
def unfriend!(resource)
|
133
|
+
redis.multi do
|
134
|
+
redis.hdel(friend_list_key, resource.id)
|
135
|
+
redis.hdel(resource.friend_list_key, self.id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def raw_friend_hashes
|
142
|
+
@_raw_friend_hashes ||= redis.hgetall(friend_list_key)
|
143
|
+
end
|
144
|
+
|
145
|
+
def raw_friend_ids
|
146
|
+
@_raw_friend_ids ||= redis.hkeys(friend_list_key)
|
147
|
+
end
|
148
|
+
|
149
|
+
def redis
|
150
|
+
@_redis ||= Friendable.redis
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Suppose the following class exists:
|
4
|
+
#
|
5
|
+
# class User < ActiveRecord::Base
|
6
|
+
# include Friendable::UserMethods
|
7
|
+
# end
|
8
|
+
|
9
|
+
describe Friendable::Friendship do
|
10
|
+
def redis; Friendable.redis; end
|
11
|
+
before(:each) { redis.flushdb }
|
12
|
+
let(:current_user) { User.first }
|
13
|
+
let(:target_user) { User.last }
|
14
|
+
|
15
|
+
describe "initialization" do
|
16
|
+
context "without options" do
|
17
|
+
subject { Friendable::Friendship.new(current_user, target_user) }
|
18
|
+
its(:created_at) { should be_nil }
|
19
|
+
its(:updated_at) { should be_nil }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "dynamic accessors" do
|
24
|
+
let(:friendship) { Friendable::Friendship.new(current_user, target_user, :foo => "bar", :hoge => "fuga") }
|
25
|
+
subject { friendship }
|
26
|
+
its(:foo) { should == "bar" }
|
27
|
+
its(:hoge) { should == "fuga" }
|
28
|
+
|
29
|
+
it "should not allow to dynamically set a new value" do
|
30
|
+
expect { friendship.new_attr = "new value" }.to raise_exception(NoMethodError)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#write_attribute" do
|
34
|
+
specify { friendship.write_attribute(:another_attr, "another value").should == "another value" }
|
35
|
+
|
36
|
+
context "after the definition" do
|
37
|
+
before { friendship.write_attribute(:another_attr, "another value") }
|
38
|
+
its(:another_attr) { should == "another value" }
|
39
|
+
specify do
|
40
|
+
friendship.another_attr = "new value"
|
41
|
+
friendship.another_attr.should == "new value"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
=begin
|
46
|
+
describe "#remove_attribute" do
|
47
|
+
specify { friendship.remove_attribute(:foo).should == "bar" }
|
48
|
+
|
49
|
+
context "after the removal" do
|
50
|
+
before { friendship.remove_attribute(:foo) }
|
51
|
+
|
52
|
+
it "can no longer get a new value from foo" do
|
53
|
+
expect { friendship.foo }.to raise_exception(NoMethodError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can no longer set a new value to foo" do
|
57
|
+
expect { friendship.foo = "bar" }.to raise_exception(NoMethodError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
=end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "serialization" do
|
65
|
+
context "with options" do
|
66
|
+
let(:friendship) { Friendable::Friendship.new(current_user, target_user, :foo => "bar", :hoge => "fuga") }
|
67
|
+
specify do
|
68
|
+
MessagePack.unpack(friendship.to_msgpack).should == {
|
69
|
+
"foo" => "bar",
|
70
|
+
"hoge" => "fuga",
|
71
|
+
"created_at" => nil,
|
72
|
+
"updated_at" => nil
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "deserialization" do
|
79
|
+
let(:current_timestamp) { Time.now.to_i }
|
80
|
+
let(:msgpacked_data) { {:created_at => current_timestamp, :updated_at => current_timestamp}.to_msgpack }
|
81
|
+
|
82
|
+
context "with options" do
|
83
|
+
subject { Friendable::Friendship.deserialize!(current_user, target_user, msgpacked_data) }
|
84
|
+
it { should be_a(Friendable::Friendship) }
|
85
|
+
its(:source_resource) { should == current_user }
|
86
|
+
its(:target_resource) { should == target_user }
|
87
|
+
its(:created_at) { should == Time.zone.at(current_timestamp) }
|
88
|
+
its(:updated_at) { should == Time.zone.at(current_timestamp) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#save" do
|
93
|
+
before { current_user.friend!(target_user) }
|
94
|
+
let(:friendship) { current_user.friendship_with(target_user) }
|
95
|
+
|
96
|
+
it "should persist the newly assigned value" do
|
97
|
+
created_at = friendship.created_at
|
98
|
+
updated_at = friendship.updated_at
|
99
|
+
sleep 1
|
100
|
+
friendship.save
|
101
|
+
|
102
|
+
friendship = current_user.friendship_with(target_user)
|
103
|
+
friendship.created_at.to_i.should == created_at.to_i
|
104
|
+
friendship.updated_at.to_i.should_not == updated_at.to_i
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#inspect" do
|
109
|
+
subject { Friendable::Friendship.new(current_user, target_user, :foo => "bar", :hoge => "fuga") }
|
110
|
+
its(:inspect) { should ==
|
111
|
+
"#<Friendable::Friendship with: User(id: #{target_user.id}), attributes: foo: bar, hoge: fuga, created_at: , updated_at: >"
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Suppose the following class exists:
|
4
|
+
#
|
5
|
+
# class User < ActiveRecord::Base
|
6
|
+
# include Friendable::UserMethods
|
7
|
+
# end
|
8
|
+
|
9
|
+
describe Friendable::UserMethods do
|
10
|
+
def redis; Friendable.redis; end
|
11
|
+
before(:each) { redis.flushdb }
|
12
|
+
let(:current_user) { User.first }
|
13
|
+
let(:target_user) { User.last }
|
14
|
+
|
15
|
+
subject { current_user }
|
16
|
+
its(:friend_list_key) { should == "Users:friend_list:1" }
|
17
|
+
|
18
|
+
describe "#friend!" do
|
19
|
+
context "to add a friend" do
|
20
|
+
before { current_user.friend!(target_user) }
|
21
|
+
specify { current_user.friend?(target_user).should be_true }
|
22
|
+
specify { redis.hkeys(current_user.friend_list_key).should include(target_user.id.to_s) }
|
23
|
+
specify { redis.hkeys(target_user.friend_list_key).should include(current_user.id.to_s) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "to add a friend with several options" do
|
27
|
+
let(:friendship) { current_user.friend!(target_user, :foo => "bar", :hoge => "fuga") }
|
28
|
+
subject { friendship }
|
29
|
+
it { should be_a(Friendable::Friendship) }
|
30
|
+
its(:friend){ should == target_user }
|
31
|
+
its(:foo){ should == "bar" }
|
32
|
+
its(:hoge){ should == "fuga" }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "to add a friend without options" do
|
36
|
+
let(:friendship) { current_user.friend!(target_user) }
|
37
|
+
subject { friendship }
|
38
|
+
it { should be_a(Friendable::Friendship) }
|
39
|
+
its(:friend){ should == target_user }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#unfriend!" do
|
44
|
+
context "to remove a friendship" do
|
45
|
+
before do
|
46
|
+
current_user.friend!(target_user)
|
47
|
+
current_user.unfriend!(target_user)
|
48
|
+
end
|
49
|
+
|
50
|
+
specify { current_user.friend?(target_user).should be_false }
|
51
|
+
specify { redis.hexists(current_user.friend_list_key, target_user.id.to_s).should be_false }
|
52
|
+
specify { redis.hexists(target_user.friend_list_key, current_user.id.to_s).should be_false }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#friends" do
|
57
|
+
subject { current_user.friends }
|
58
|
+
|
59
|
+
context "without friends" do
|
60
|
+
it { should be_an(ActiveRecord::Relation) }
|
61
|
+
its(:count){ should == 0 }
|
62
|
+
end
|
63
|
+
|
64
|
+
context "with one friend" do
|
65
|
+
before { current_user.friend!(target_user) }
|
66
|
+
it { should include(target_user) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#friendships" do
|
71
|
+
subject { current_user.friendships }
|
72
|
+
|
73
|
+
context "without friends to return an empty erray" do
|
74
|
+
it { should be_an(Array) }
|
75
|
+
its(:count) { should == 0 }
|
76
|
+
end
|
77
|
+
|
78
|
+
context "with one friend to return an array of friendship objects" do
|
79
|
+
before { current_user.friend!(target_user, :foo => "bar", :hoge => "fuga") }
|
80
|
+
subject { current_user.friendships.first }
|
81
|
+
it { should be_a(Friendable::Friendship) }
|
82
|
+
its(:source_resource) { should == current_user }
|
83
|
+
its(:target_resource) { should == target_user }
|
84
|
+
its(:created_at) { should be_a(ActiveSupport::TimeWithZone) }
|
85
|
+
its(:updated_at) { should be_a(ActiveSupport::TimeWithZone) }
|
86
|
+
its(:foo) { should == "bar" }
|
87
|
+
its(:hoge) { should == "fuga" }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#friendship_with" do
|
92
|
+
context "without friends" do
|
93
|
+
specify do
|
94
|
+
expect {
|
95
|
+
current_user.friendship_with(target_user)
|
96
|
+
}.to raise_exception(Friendable::FriendshipNotFound)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "with one friend" do
|
101
|
+
before { current_user.friend!(target_user, :foo => "bar", :hoge => "fuga") }
|
102
|
+
subject { current_user.friendship_with(target_user) }
|
103
|
+
it { should be_a(Friendable::Friendship) }
|
104
|
+
its(:foo) { should == "bar" }
|
105
|
+
its(:hoge) { should == "fuga" }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#friend_ids" do
|
110
|
+
subject { current_user.friend_ids }
|
111
|
+
|
112
|
+
context "without friends" do
|
113
|
+
it { should be_an(Array) }
|
114
|
+
its(:count) { should == 0 }
|
115
|
+
end
|
116
|
+
|
117
|
+
context "with one friend" do
|
118
|
+
before { current_user.friend!(target_user) }
|
119
|
+
it { should include(target_user.id) }
|
120
|
+
end
|
121
|
+
|
122
|
+
context "without friends after unfriend" do
|
123
|
+
before do
|
124
|
+
current_user.friend!(target_user)
|
125
|
+
current_user.unfriend!(target_user)
|
126
|
+
end
|
127
|
+
|
128
|
+
it { should_not include(target_user.id) }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#friends_count" do
|
133
|
+
subject { current_user.friends_count }
|
134
|
+
|
135
|
+
context "without friends" do
|
136
|
+
it { should == 0 }
|
137
|
+
end
|
138
|
+
|
139
|
+
context "with one friend" do
|
140
|
+
before { current_user.friend!(target_user) }
|
141
|
+
it { should == 1 }
|
142
|
+
end
|
143
|
+
|
144
|
+
context "without friends after unfriend" do
|
145
|
+
before do
|
146
|
+
current_user.friend!(target_user)
|
147
|
+
current_user.unfriend!(target_user)
|
148
|
+
end
|
149
|
+
|
150
|
+
it { should == 0 }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Friendable do
|
4
|
+
it { should respond_to :redis }
|
5
|
+
it { should respond_to :resource_class }
|
6
|
+
it { should respond_to :resource_class= }
|
7
|
+
its(:redis) { should be_a(Redis::Namespace) }
|
8
|
+
|
9
|
+
describe "confgiruration" do
|
10
|
+
context "with the default redis" do
|
11
|
+
subject { Friendable.redis.client }
|
12
|
+
it { should be_a(Redis::Client) }
|
13
|
+
its(:host) { should == "127.0.0.1" }
|
14
|
+
its(:port) { should == 6379 }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "with a string" do
|
18
|
+
before { Friendable.redis = "10.0.0.1:6379:1" }
|
19
|
+
subject { Friendable.redis.client }
|
20
|
+
it { should be_a(Redis::Client) }
|
21
|
+
its(:host) { should == "10.0.0.1" }
|
22
|
+
its(:port) { should == 6379 }
|
23
|
+
its(:db) { should == 1 }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with an object of Redis class" do
|
27
|
+
before { Friendable.redis = Redis.new(:host => "127.0.0.1", :port => 6379) }
|
28
|
+
subject { Friendable.redis.client }
|
29
|
+
it { should be_a(Redis::Client) }
|
30
|
+
its(:host) { should == "127.0.0.1" }
|
31
|
+
its(:port) { should == 6379 }
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with an object of Redis::Namespace class" do
|
35
|
+
before { Friendable.redis = Redis::Namespace.new(:friendable, :host => "127.0.0.1", :port => 6379) }
|
36
|
+
subject { Friendable.redis.client }
|
37
|
+
it { should be_a(Redis::Client) }
|
38
|
+
its(:host) { should == "127.0.0.1" }
|
39
|
+
its(:port) { should == 6379 }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
Bundler.require
|
8
|
+
|
9
|
+
# database
|
10
|
+
require 'active_record'
|
11
|
+
ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
|
12
|
+
ActiveRecord::Base.establish_connection('test')
|
13
|
+
|
14
|
+
# migrations
|
15
|
+
class CreateAllTables < ActiveRecord::Migration
|
16
|
+
def self.up
|
17
|
+
create_table(:users) {|t| t.string :name; t.integer :age}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
ActiveRecord::Migration.verbose = false
|
21
|
+
CreateAllTables.up
|
22
|
+
|
23
|
+
# models
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
include Friendable::UserMethods
|
26
|
+
end
|
27
|
+
User.create!(:name => "Some User")
|
28
|
+
User.create!(:name => "Another User")
|
29
|
+
|
30
|
+
# default time sone
|
31
|
+
Time.zone = "UTC"
|
32
|
+
|
33
|
+
# Requires supporting files with custom matchers and macros, etc,
|
34
|
+
# in ./support/ and its subdirectories.
|
35
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f }
|
36
|
+
|
37
|
+
RSpec.configure do |config|
|
38
|
+
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: friendable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yuki Nishijima
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: msgpack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activesupport
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.0.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.0.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis-namespace
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: keytar
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: activerecord
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: sqlite3
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Redis backed friendship engine for your Ruby models
|
127
|
+
email:
|
128
|
+
- mail@yukinishijima.net
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- .rspec
|
135
|
+
- .rvmrc
|
136
|
+
- .simplecov
|
137
|
+
- .travis.yml
|
138
|
+
- Gemfile
|
139
|
+
- LICENSE.md
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- friendable.gemspec
|
143
|
+
- lib/friendable.rb
|
144
|
+
- lib/friendable/exceptions.rb
|
145
|
+
- lib/friendable/friendship.rb
|
146
|
+
- lib/friendable/user_methods.rb
|
147
|
+
- lib/friendable/version.rb
|
148
|
+
- spec/friendable/friendship_spec.rb
|
149
|
+
- spec/friendable/user_methods_spec.rb
|
150
|
+
- spec/friendable_spec.rb
|
151
|
+
- spec/spec_helper.rb
|
152
|
+
homepage: https://github.com/yuki24/friendable
|
153
|
+
licenses: []
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
none: false
|
166
|
+
requirements:
|
167
|
+
- - ! '>='
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 1.8.24
|
173
|
+
signing_key:
|
174
|
+
specification_version: 3
|
175
|
+
summary: Redis backed friendship engine for your Ruby models
|
176
|
+
test_files:
|
177
|
+
- spec/friendable/friendship_spec.rb
|
178
|
+
- spec/friendable/user_methods_spec.rb
|
179
|
+
- spec/friendable_spec.rb
|
180
|
+
- spec/spec_helper.rb
|