friendable 0.1.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/.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
|