simple-feed 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +14 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +18 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +457 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/examples/hash_provider_example.rb +24 -0
  16. data/examples/redis_provider_example.rb +28 -0
  17. data/examples/shared/provider_example.rb +66 -0
  18. data/lib/simple-feed.rb +1 -0
  19. data/lib/simple_feed.rb +1 -0
  20. data/lib/simplefeed.rb +61 -0
  21. data/lib/simplefeed/activity/base.rb +14 -0
  22. data/lib/simplefeed/activity/multi_user.rb +71 -0
  23. data/lib/simplefeed/activity/single_user.rb +70 -0
  24. data/lib/simplefeed/dsl.rb +38 -0
  25. data/lib/simplefeed/dsl/activities.rb +70 -0
  26. data/lib/simplefeed/dsl/formatter.rb +109 -0
  27. data/lib/simplefeed/event.rb +87 -0
  28. data/lib/simplefeed/feed.rb +78 -0
  29. data/lib/simplefeed/providers.rb +45 -0
  30. data/lib/simplefeed/providers/base/provider.rb +84 -0
  31. data/lib/simplefeed/providers/hash.rb +8 -0
  32. data/lib/simplefeed/providers/hash/paginator.rb +31 -0
  33. data/lib/simplefeed/providers/hash/provider.rb +169 -0
  34. data/lib/simplefeed/providers/proxy.rb +38 -0
  35. data/lib/simplefeed/providers/redis.rb +9 -0
  36. data/lib/simplefeed/providers/redis/boot_info.yml +99 -0
  37. data/lib/simplefeed/providers/redis/driver.rb +158 -0
  38. data/lib/simplefeed/providers/redis/provider.rb +255 -0
  39. data/lib/simplefeed/providers/redis/stats.rb +85 -0
  40. data/lib/simplefeed/providers/serialization/key.rb +82 -0
  41. data/lib/simplefeed/response.rb +77 -0
  42. data/lib/simplefeed/version.rb +3 -0
  43. data/man/running-the-example.png +0 -0
  44. data/man/sf-example.png +0 -0
  45. data/simple-feed.gemspec +44 -0
  46. 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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -0,0 +1 @@
1
+ require 'simplefeed'
@@ -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,14 @@
1
+ module SimpleFeed
2
+ module Activity
3
+ class Base
4
+ attr_reader :feed
5
+
6
+ def initialize(feed:)
7
+ @feed = feed
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'multi_user'
14
+ require_relative 'single_user'
@@ -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