simple-feed 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +14 -0
- data/.yardopts +3 -0
- data/Gemfile +4 -0
- data/Guardfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +457 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/hash_provider_example.rb +24 -0
- data/examples/redis_provider_example.rb +28 -0
- data/examples/shared/provider_example.rb +66 -0
- data/lib/simple-feed.rb +1 -0
- data/lib/simple_feed.rb +1 -0
- data/lib/simplefeed.rb +61 -0
- data/lib/simplefeed/activity/base.rb +14 -0
- data/lib/simplefeed/activity/multi_user.rb +71 -0
- data/lib/simplefeed/activity/single_user.rb +70 -0
- data/lib/simplefeed/dsl.rb +38 -0
- data/lib/simplefeed/dsl/activities.rb +70 -0
- data/lib/simplefeed/dsl/formatter.rb +109 -0
- data/lib/simplefeed/event.rb +87 -0
- data/lib/simplefeed/feed.rb +78 -0
- data/lib/simplefeed/providers.rb +45 -0
- data/lib/simplefeed/providers/base/provider.rb +84 -0
- data/lib/simplefeed/providers/hash.rb +8 -0
- data/lib/simplefeed/providers/hash/paginator.rb +31 -0
- data/lib/simplefeed/providers/hash/provider.rb +169 -0
- data/lib/simplefeed/providers/proxy.rb +38 -0
- data/lib/simplefeed/providers/redis.rb +9 -0
- data/lib/simplefeed/providers/redis/boot_info.yml +99 -0
- data/lib/simplefeed/providers/redis/driver.rb +158 -0
- data/lib/simplefeed/providers/redis/provider.rb +255 -0
- data/lib/simplefeed/providers/redis/stats.rb +85 -0
- data/lib/simplefeed/providers/serialization/key.rb +82 -0
- data/lib/simplefeed/response.rb +77 -0
- data/lib/simplefeed/version.rb +3 -0
- data/man/running-the-example.png +0 -0
- data/man/sf-example.png +0 -0
- data/simple-feed.gemspec +44 -0
- metadata +333 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
|
7
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
8
|
+
t.files = %w(lib/**/*.rb exe/*.rb - README.md LICENSE.txt)
|
9
|
+
t.options.unshift('--title', '"SimpleFeed — Fast and Scalable "write-time" Simple Feed for Social Networks, with a Redis-based default backend implementation."')
|
10
|
+
t.after = ->() { exec('open doc/index.html') } if RUBY_PLATFORM =~ /darwin/
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
RSpec::Core::RakeTask.new(:spec)
|
15
|
+
|
16
|
+
task :default => :spec
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "activity/feed"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is an executable example of working with a Redis feed, and storing
|
4
|
+
# events for various users.
|
5
|
+
#
|
6
|
+
# DEPENDENCIES:
|
7
|
+
# gem install colored2
|
8
|
+
# gem install awesome_print
|
9
|
+
#
|
10
|
+
# RUNNING
|
11
|
+
# ruby redis-feed.rb
|
12
|
+
#
|
13
|
+
|
14
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
15
|
+
|
16
|
+
require 'simplefeed'
|
17
|
+
|
18
|
+
@feed = SimpleFeed.define(:news) do |f|
|
19
|
+
f.provider = SimpleFeed.provider(:hash)
|
20
|
+
f.max_size = 1000
|
21
|
+
end
|
22
|
+
|
23
|
+
require_relative 'shared/provider_example'
|
24
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is an executable example of working with a Redis feed, and storing
|
4
|
+
# events for various users.
|
5
|
+
#
|
6
|
+
# DEPENDENCIES:
|
7
|
+
# gem install colored2
|
8
|
+
# gem install awesome_print
|
9
|
+
#
|
10
|
+
# RUNNING
|
11
|
+
# ruby examples/redis-feed.rb [ number_of_users ]
|
12
|
+
#
|
13
|
+
# TO SEE ALL REDIS COMMANDS:
|
14
|
+
# export REDIS_DEBUG=1
|
15
|
+
# ruby examples/redis-feed.rb
|
16
|
+
|
17
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
18
|
+
|
19
|
+
require 'simplefeed'
|
20
|
+
|
21
|
+
@feed = SimpleFeed.define(:news) do |f|
|
22
|
+
f.provider = SimpleFeed.provider(:redis,
|
23
|
+
redis: -> { Redis.new },
|
24
|
+
pool_size: 10,
|
25
|
+
batch_size: 10)
|
26
|
+
end
|
27
|
+
|
28
|
+
require_relative 'shared/provider_example'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
|
2
|
+
|
3
|
+
# Please set @feed in the enclosing context
|
4
|
+
|
5
|
+
raise ArgumentError, 'No @feed defined in the enclosing example' unless defined?(@feed)
|
6
|
+
|
7
|
+
require 'simplefeed'
|
8
|
+
|
9
|
+
@number_of_users = ARGV[0] ? ARGV[0].to_i : 1
|
10
|
+
@users = Array.new(@number_of_users) { rand(200_000_000...800_000_000) }
|
11
|
+
@activity = @feed.activity(@users)
|
12
|
+
@uid = @users.first
|
13
|
+
|
14
|
+
include SimpleFeed::DSL
|
15
|
+
|
16
|
+
class Object
|
17
|
+
def _v
|
18
|
+
self.to_s.bold.red
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def p(*args)
|
23
|
+
printf "%-40s %s\n", args[0].blue, args[1].bold.red
|
24
|
+
end
|
25
|
+
|
26
|
+
with_activity(@activity) do
|
27
|
+
header "#{@activity.feed.provider_type.to_s} provider example"
|
28
|
+
wipe { puts 'wiping feed...' }
|
29
|
+
|
30
|
+
store('value one') { p 'storing', 'value one' }
|
31
|
+
store('value two') { p 'storing', 'value two' }
|
32
|
+
store('value three') { p 'storing', 'value three' }
|
33
|
+
hr
|
34
|
+
|
35
|
+
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
36
|
+
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
37
|
+
|
38
|
+
header 'FIRST PAGE (PER-PAGE: 2) #to_json'
|
39
|
+
paginate(page: 1, per_page: 2) { |r| puts r[@uid].map(&:to_json) }
|
40
|
+
header 'SECOND PAGE (PER-PAGE: 2) #to_s'
|
41
|
+
paginate(page: 2, per_page: 2) { |r| puts r[@uid].map(&:to_s) }
|
42
|
+
header 'LAST PAGE (PER-PAGE: 1) #to_color_s'
|
43
|
+
paginate(page: 3, per_page: 1) { |r| puts r[@uid].map(&:to_color_s) }
|
44
|
+
|
45
|
+
hr
|
46
|
+
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
47
|
+
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
48
|
+
|
49
|
+
hr
|
50
|
+
store('value four') { p 'storing', 'value four' }
|
51
|
+
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
52
|
+
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
53
|
+
|
54
|
+
color_dump
|
55
|
+
|
56
|
+
hr
|
57
|
+
delete('value three') { p 'deleting', 'value three' }
|
58
|
+
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
59
|
+
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
60
|
+
hr
|
61
|
+
delete('value four') { p 'deleting', 'value four' }
|
62
|
+
total_count { |r| p 'total_count is now', "#{r[@uid]._v}" }
|
63
|
+
unread_count { |r| p 'unread_count is now', "#{r[@uid]._v}" }
|
64
|
+
|
65
|
+
end
|
66
|
+
|
data/lib/simple-feed.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'simplefeed'
|
data/lib/simple_feed.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'simplefeed'
|
data/lib/simplefeed.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'hashie/extensions/symbolize_keys'
|
2
|
+
require 'simplefeed/version'
|
3
|
+
|
4
|
+
::Dir.glob(::File.expand_path('../simplefeed/*.rb', __FILE__)).each { |f| require_relative(f) }
|
5
|
+
|
6
|
+
require 'simplefeed/providers/redis'
|
7
|
+
require 'simplefeed/providers/hash'
|
8
|
+
require 'simplefeed/dsl'
|
9
|
+
|
10
|
+
Float.class_eval do
|
11
|
+
def near_eql?(other)
|
12
|
+
delta = 0.001
|
13
|
+
n = (other - self).abs
|
14
|
+
delta >= n
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module SimpleFeed
|
19
|
+
@registry = {}
|
20
|
+
|
21
|
+
def self.registry
|
22
|
+
@registry
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.define(name, **options, &block)
|
26
|
+
name = name.to_sym unless name.is_a?(Symbol)
|
27
|
+
feed = registry[name] ? registry[name] : SimpleFeed::Feed.new(name)
|
28
|
+
feed.configure(options) do
|
29
|
+
block.call(feed) if block
|
30
|
+
end
|
31
|
+
registry[name] = feed
|
32
|
+
feed
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.get(name)
|
36
|
+
registry[name.to_sym]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.provider(provider_name, *args, **opts, &block)
|
40
|
+
provider_class = SimpleFeed::Providers.registry[provider_name]
|
41
|
+
raise ArgumentError, "No provider named #{provider_name} was found, #{SimpleFeed::Providers.registry.inspect}" unless provider_class
|
42
|
+
provider_class.new(*args, **opts, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
# Forward all other method calls to Provider
|
47
|
+
def method_missing(name, *args, &block)
|
48
|
+
registry[name] || super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns list of class attributes based on the setter methods.
|
53
|
+
# Not fool-proof, but works in this context.
|
54
|
+
def self.class_attributes(klass)
|
55
|
+
klass.instance_methods.grep(%r{[^=!]=$}).map { |m| m.to_s.gsub(/=/, '').to_sym }
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.symbolize!(hash)
|
59
|
+
Hashie::Extensions::SymbolizeKeys.symbolize_keys!(hash)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module SimpleFeed
|
5
|
+
module Activity
|
6
|
+
class MultiUser < Base
|
7
|
+
|
8
|
+
attr_reader :user_ids
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@user_ids, :size, :each, :select, :delete_if, :inject
|
12
|
+
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
#
|
16
|
+
# Multi-user API for the feeds.
|
17
|
+
#
|
18
|
+
# ```ruby
|
19
|
+
# @multi = SimpleFeed.get(:feed_name).for(User.active.map(&:id))
|
20
|
+
#
|
21
|
+
# @multi.store(value:, at:)
|
22
|
+
# @multi.store(event:)
|
23
|
+
# # => [Response] { user_id => [Boolean], ... } true if the value was stored, false if it wasn't.
|
24
|
+
#
|
25
|
+
# @multi.delete(value:, at:)
|
26
|
+
# @multi.delete(event:)
|
27
|
+
# # => [Response] { user_id => [Boolean], ... } true if the value was deleted, false if it didn't exist
|
28
|
+
#
|
29
|
+
# @multi.wipe
|
30
|
+
# # => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
|
31
|
+
#
|
32
|
+
# @multi.paginate(page:, per_page:, peek: false)
|
33
|
+
# # => [Response] { user_id => [Array]<Event>, ... }
|
34
|
+
#
|
35
|
+
# # With (peak: true) does not reset last_read, otherwise it does.
|
36
|
+
#
|
37
|
+
# @multi.fetch
|
38
|
+
# # => [Response] { user_id => [Array]<Event>, ... }
|
39
|
+
#
|
40
|
+
# @multi.reset_last_read
|
41
|
+
# # => [Response] { user_id => [Time] last_read, ... }
|
42
|
+
#
|
43
|
+
# @multi.total_count
|
44
|
+
# # => [Response] { user_id => [Integer] total_count, ... }
|
45
|
+
#
|
46
|
+
# @multi.unread_count
|
47
|
+
# # => [Response] { user_id => [Integer] unread_count, ... }
|
48
|
+
#
|
49
|
+
# @multi.last_read
|
50
|
+
# # => [Response] { user_id => [Time] last_read, ... }
|
51
|
+
# ```
|
52
|
+
|
53
|
+
SimpleFeed::Providers.define_provider_methods(self) do |instance, method, *args, **opts, &block|
|
54
|
+
opts.merge!(user_ids: instance.user_ids)
|
55
|
+
if opts[:event]
|
56
|
+
opts.merge!(value: opts[:event].value, at: opts[:event].at)
|
57
|
+
opts.delete(:event)
|
58
|
+
end
|
59
|
+
response = instance.feed.send(method, *args, **opts, &block)
|
60
|
+
yield(response) if block_given?
|
61
|
+
raise StandardError, "Nil response from provider #{instance.feed.provider&.provider&.class}, method #{method}(#{opts})" unless response
|
62
|
+
response
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize(feed:, user_ids:)
|
66
|
+
super(feed: feed)
|
67
|
+
@user_ids = user_ids
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module SimpleFeed
|
4
|
+
module Activity
|
5
|
+
|
6
|
+
# Lazy implementation of SingleUser based on delegating an array of
|
7
|
+
# one user_id to +MultiUser+
|
8
|
+
|
9
|
+
class SingleUser < ::SimpleFeed::Activity::Base
|
10
|
+
attr_reader :user_id
|
11
|
+
attr_accessor :user_activity
|
12
|
+
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
def each
|
16
|
+
yield(user_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Single-user API for the feeds.
|
21
|
+
#
|
22
|
+
# @ua = SimpleFeed.get(:feed_name).user_activity(current_user.id)
|
23
|
+
#
|
24
|
+
# @ua.store(value:, at:)
|
25
|
+
# # => [Boolean] true if the value was stored, false if it wasn't.
|
26
|
+
#
|
27
|
+
# @ua.delete(value:, at:)
|
28
|
+
# # => [Boolean] true if the value was deleted, false if it didn't exist
|
29
|
+
#
|
30
|
+
# @ua.wipe
|
31
|
+
# # => [Boolean] true
|
32
|
+
#
|
33
|
+
# @ua.paginate(page:, per_page:, peek: false)
|
34
|
+
# # => [Array]<Event>
|
35
|
+
# # with peak: true does not reset last_read
|
36
|
+
#
|
37
|
+
# @ua.fetch
|
38
|
+
# # => [Array]<Event>
|
39
|
+
#
|
40
|
+
# @ua.reset_last_read
|
41
|
+
# # => [Time] last_read
|
42
|
+
#
|
43
|
+
# @ua.total_count
|
44
|
+
# # => [Integer] total_count
|
45
|
+
#
|
46
|
+
# @ua.unread_count
|
47
|
+
# # => [Integer] unread_count
|
48
|
+
#
|
49
|
+
# @ua.last_read
|
50
|
+
# # => [Time] last_read
|
51
|
+
# ```
|
52
|
+
|
53
|
+
SimpleFeed::Providers.define_provider_methods(self) do |instance, method, *args, **opts, &block|
|
54
|
+
response = instance.user_activity.send(method, *args, **opts, &block)
|
55
|
+
unless response.has_user?(instance.user_id)
|
56
|
+
raise StandardError, "Nil response from provider #{instance.feed.provider&.provider&.class}, method #{method}(#{opts}) — user_id #{instance.user_id}"
|
57
|
+
end
|
58
|
+
response = response[instance.user_id]
|
59
|
+
yield(response) if block_given?
|
60
|
+
response
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(user_id:, feed:)
|
64
|
+
@feed = feed
|
65
|
+
@user_id = user_id
|
66
|
+
self.user_activity = MultiUser.new(feed: feed, user_ids: [user_id])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'providers'
|
2
|
+
require 'simplefeed'
|
3
|
+
require_relative 'dsl/activities'
|
4
|
+
|
5
|
+
module SimpleFeed
|
6
|
+
# This module offers a convenient DSL-based approach to manipulating user feeds.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# require 'simplefeed/dsl'
|
11
|
+
# include SimpleFeed::DSL
|
12
|
+
#
|
13
|
+
# with_activity(SimpleFeed.get(:newsfeed).activity(user_id)) do
|
14
|
+
# puts 'success' if store(value: 'hello', at: Time.now)
|
15
|
+
# puts fetch
|
16
|
+
# puts total_count
|
17
|
+
# puts unread_count
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
module DSL
|
21
|
+
class << self
|
22
|
+
attr_accessor :debug
|
23
|
+
|
24
|
+
def debug?
|
25
|
+
self.debug
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_activity(activity, **opts, &block)
|
30
|
+
opts.merge!({ context: self }) unless opts && opts[:context]
|
31
|
+
SimpleFeed::DSL::Activities.new(activity, **opts).instance_eval(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def event(value, at = Time.now)
|
35
|
+
SimpleFeed::Event.new(value, at)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'formatter'
|
2
|
+
module SimpleFeed
|
3
|
+
module DSL
|
4
|
+
class Activities
|
5
|
+
|
6
|
+
include SimpleFeed::DSL::Formatter
|
7
|
+
|
8
|
+
attr_accessor :activity, :feed
|
9
|
+
|
10
|
+
def initialize(activity, **opts)
|
11
|
+
self.activity = activity
|
12
|
+
self.feed = activity.feed
|
13
|
+
opts.each_pair do |key, value|
|
14
|
+
self.class.instance_eval do
|
15
|
+
attr_accessor key
|
16
|
+
end
|
17
|
+
self.send("#{key}=".to_sym, value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates wrapper methods around the API and optionally prints both calls and return values
|
22
|
+
#
|
23
|
+
# def store(event: .., | value:, at: )
|
24
|
+
# activity.store(**opts)
|
25
|
+
# end
|
26
|
+
# etc...
|
27
|
+
SimpleFeed::Providers.define_provider_methods(self) do |instance, method, *args, **opts, &block|
|
28
|
+
if args&.first
|
29
|
+
arg1 = args.shift
|
30
|
+
if arg1.is_a?(SimpleFeed::Event)
|
31
|
+
event = arg1
|
32
|
+
else
|
33
|
+
opts[:value] = arg1 unless opts[:value]
|
34
|
+
opts[:at] = args.shift unless opts[:value]
|
35
|
+
end
|
36
|
+
else
|
37
|
+
event = opts.delete(:event)
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.merge!(value: event.value, at: event.at) if event
|
41
|
+
|
42
|
+
response = instance.instance_eval do
|
43
|
+
print_debug_info(method, **opts) do
|
44
|
+
activity.send(method, *args, **opts)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if block
|
49
|
+
if instance.context
|
50
|
+
instance.context.instance_exec(response, &block)
|
51
|
+
else
|
52
|
+
block.call(response)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
response
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def print_debug_info(method, **opts)
|
62
|
+
brackets = opts.empty? ? ['', ''] : %w{( )}
|
63
|
+
printf "\n#{self.feed.name.to_s.blue}.#{method.to_s.cyan.bold}#{brackets[0]}#{opts.to_s.gsub(/[{}]/, '').blue}#{brackets[1]} \n" if SimpleFeed::DSL.debug?
|
64
|
+
response = yield if block_given?
|
65
|
+
puts response.inspect.yellow if SimpleFeed::DSL.debug?
|
66
|
+
response
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|