simple-feed 1.0.2
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.
- 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
|