simple-feed 2.1.0 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tty-box'
4
+ require 'tty-screen'
3
5
  require 'simplefeed/dsl'
4
6
  require 'simplefeed/activity/single_user'
5
7
  require 'simplefeed/activity/multi_user'
8
+ require 'awesome_print'
9
+
6
10
  module SimpleFeed
7
11
  module DSL
8
12
  # This module exports method #color_dump which receives an activity and
@@ -18,40 +22,43 @@ module SimpleFeed
18
22
  this_activity.feed.activity([this_activity.user_id])
19
23
  else
20
24
  this_activity
21
- end
25
+ end
22
26
  _puts
23
27
 
24
- header do
25
- field('Feed Name', feed.name, "\n")
26
- field('Provider', feed.provider.provider.class, "\n")
27
- field('Max Size', feed.max_size, "\n")
28
+ feed_header(feed) do
29
+ [
30
+ field('Feed Name', feed.name, "\n"),
31
+ field('Provider', feed.provider.provider.class, "\n"),
32
+ field('Max Size', feed.max_size, "\n")
33
+ ]
28
34
  end
29
35
 
30
36
  with_activity(this_activity) do
31
- this_activity.each do |user_id|
37
+ this_activity.each_with_index do |user_id, index|
32
38
  this_last_event_at = nil
33
39
  this_last_read = (last_read[user_id] || 0.0).to_f
34
40
 
41
+ fields = []
35
42
  [['User ID', user_id, "\n"],
36
43
  ['Activities', sprintf('%d total, %d unread', total_count[user_id], unread_count[user_id]), "\n"],
37
44
  ['Last Read', this_last_read ? Time.at(this_last_read) : 'N/A'],].each do |field, value, *args|
38
- field(field, value, *args)
45
+ fields << field(field, value, *args)
39
46
  end
40
47
 
41
- _puts; hr '¨'
48
+ header(title: { top_center: " « User Activity #{index + 1} » " }, style: { fg: :green }) { fields }
42
49
 
43
50
  this_events = fetch[user_id]
44
51
  this_events_count = this_events.size
45
- this_events.each_with_index do |_event, _index|
46
- if this_last_event_at.nil? && _event.at < this_last_read
52
+ this_events.each_with_index do |evt, idx|
53
+ if this_last_event_at.nil? && evt.at < this_last_read
47
54
  print_last_read_separator(this_last_read)
48
- elsif this_last_event_at && this_last_read < this_last_event_at && this_last_read > _event.at
55
+ elsif this_last_event_at && this_last_read < this_last_event_at && this_last_read > evt.at
49
56
  print_last_read_separator(this_last_read)
50
57
  end
51
58
 
52
- this_last_event_at = _event.at # float
53
- _print "[%2d] %16s %s\n", _index, _event.time.strftime(TIME_FORMAT).blue.bold, _event.value
54
- if _index == this_events_count - 1 && this_last_read < _event.at
59
+ this_last_event_at = evt.at # float
60
+ output "[%2d] %16s %s\n", idx, evt.time.strftime(TIME_FORMAT).blue.bold, evt.value
61
+ if idx == this_events_count - 1 && this_last_read < evt.at
55
62
  print_last_read_separator(this_last_read)
56
63
  end
57
64
  end
@@ -60,17 +67,18 @@ module SimpleFeed
60
67
  end
61
68
 
62
69
  def print_last_read_separator(lr)
63
- _print ">>>> %16s <<<< last read\n", Time.at(lr).strftime(TIME_FORMAT).red.bold
70
+ output "———— %16s [last read] ———————————— \n", Time.at(lr).strftime(TIME_FORMAT).red.bold
64
71
  end
65
72
  end
66
73
 
74
+ # This allows redirecting output in tests.
67
75
  @print_method = :printf
68
76
 
69
77
  class << self
70
78
  attr_accessor :print_method
71
79
  end
72
80
 
73
- def _print(*args, **opts, &block)
81
+ def output(*args, **opts, &block)
74
82
  send(SimpleFeed::DSL.print_method, *args, **opts, &block)
75
83
  end
76
84
 
@@ -82,8 +90,6 @@ module SimpleFeed
82
90
  sprintf ' %20s ', text
83
91
  end
84
92
 
85
- TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
86
-
87
93
  def field_value(value)
88
94
  case value
89
95
  when Numeric
@@ -95,22 +101,55 @@ module SimpleFeed
95
101
  end
96
102
  end
97
103
 
98
- def field(label, value, sep = '')
99
- _print field_label(label).italic + field_value(value).cyan.bold + sep
104
+ def field(label, value, _sep = '')
105
+ field_label(label) + ' ❯ ' + field_value(value)
106
+ end
107
+
108
+ def hr
109
+ output hr_string.magenta
110
+ end
111
+
112
+ def hr_string
113
+ '―' * width + "\n"
114
+ end
115
+
116
+ def width
117
+ @width ||= [[TTY::Screen.width - 5, 60].max, 75].min
100
118
  end
101
119
 
102
- def hr(char = '—')
103
- _print(_hr(char).magenta)
120
+ def feed_header(feed, &block)
121
+ header title: { top_left: " « #{feed.name.capitalize} Feed » " },
122
+ border: :thick,
123
+ style: {
124
+ fg: :black,
125
+ bg: :green,
126
+ border: { fg: :bright_black, bg: :green }
127
+ }, &block
104
128
  end
105
129
 
106
- def _hr(char = '—')
107
- char * 75 + "\n"
130
+ def header(*args, **opts)
131
+ message = args.join("\n")
132
+ msg = block_given? ? (yield || message) : message + "\n"
133
+ config = box_config(**opts)
134
+ lines = Array(msg).flatten
135
+ box = TTY::Box.frame(*lines, **config)
136
+ output "\n#{box}"
108
137
  end
109
138
 
110
- def header(message = nil)
111
- _print(_hr.green.bold)
112
- block_given? ? yield : _print(message.green.italic + "\n")
113
- _print(_hr.green.bold)
139
+ private
140
+
141
+ def box_config(**opts)
142
+ {
143
+ width: width,
144
+ align: :left,
145
+ padding: [1, 3],
146
+ style: {
147
+ fg: :bright_yellow,
148
+ border: {
149
+ fg: :bright_magenta,
150
+ }
151
+ }
152
+ }.merge(opts)
114
153
  end
115
154
  end
116
155
  end
@@ -5,8 +5,24 @@ require 'json'
5
5
  module SimpleFeed
6
6
  class Event
7
7
  attr_accessor :value, :at
8
+
8
9
  include Comparable
9
10
 
11
+ class << self
12
+ attr_accessor :is_time
13
+ end
14
+
15
+ # This proc can be overridden in a configuration if needed.
16
+ # @example To always assume this is time, set it like so,
17
+ # before defining your feeds.
18
+ #
19
+ # SimpleFeed::Event.is_time = ->(*) { true }
20
+ #
21
+ self.is_time = ->(float) {
22
+ # assume it's time if epoch is > June 1974 and < December 2040.
23
+ float < 2_237_932_800.0 && float > 139_276_800.0
24
+ }
25
+
10
26
  def initialize(*args, value: nil, at: Time.now)
11
27
  if args && !args.empty?
12
28
  self.value = args[0]
@@ -22,7 +38,11 @@ module SimpleFeed
22
38
  end
23
39
 
24
40
  def time
41
+ return nil unless Event.is_time[at]
42
+
25
43
  Time.at(at)
44
+ rescue ArgumentError
45
+ nil
26
46
  end
27
47
 
28
48
  def <=>(other)
@@ -38,10 +58,6 @@ module SimpleFeed
38
58
  self.value == other.value
39
59
  end
40
60
 
41
- def to_h
42
- { value: value, at: at, time: time }
43
- end
44
-
45
61
  def hash
46
62
  self.value.hash
47
63
  end
@@ -54,16 +70,38 @@ module SimpleFeed
54
70
  YAML.dump(to_h)
55
71
  end
56
72
 
73
+ def to_h
74
+ return @to_h if @to_h
75
+
76
+ @to_h ||= { value: value, at: at }
77
+ @to_h.merge!(time: time) if time
78
+ @to_h
79
+ end
80
+
57
81
  def to_s
58
- "<SimpleFeed::Event: value='#{value}', at='#{at}', time='#{time}'>"
82
+ return @to_s if @to_s
83
+
84
+ output = StringIO.new
85
+ output.print "<SimpleFeed::Event: "
86
+ output.print(time.nil? ? "[#{at}]" : "[#{time&.strftime(::SimpleFeed::TIME_FORMAT)}]")
87
+ output.print ", [\"#{value}\"]"
88
+ @to_s = output.string
59
89
  end
60
90
 
91
+ COLOR_MAP = {
92
+ 1 => ->(word) { word.green.bold },
93
+ 3 => ->(word) { word.yellow.bold },
94
+ }.freeze
95
+
61
96
  def to_color_s
62
- counter = 0
63
- to_s.split(/[']/).map do |word|
64
- counter += 1
65
- counter.even? ? word.yellow.bold : word.blue
66
- end.join('')
97
+ return @to_color_s if @to_color_s
98
+
99
+ output = StringIO.new
100
+ to_s.split(/[\[\]]/).each_with_index do |word, index|
101
+ output.print(COLOR_MAP[index]&.call(word) || word.cyan)
102
+ end
103
+ output.print '>'
104
+ @to_color_s = output.string
67
105
  end
68
106
 
69
107
  def inspect
@@ -2,11 +2,17 @@
2
2
 
3
3
  require_relative 'providers'
4
4
  require_relative 'activity/base'
5
- require 'simplefeed/key/template'
5
+ require_relative 'providers/key'
6
6
 
7
7
  module SimpleFeed
8
8
  class Feed
9
- attr_accessor :per_page, :max_size, :batch_size, :meta, :namespace
9
+ attr_accessor :per_page,
10
+ :max_size,
11
+ :batch_size,
12
+ :namespace,
13
+ :data_key_transformer,
14
+ :meta_key_transformer
15
+
10
16
  attr_reader :name
11
17
 
12
18
  SimpleFeed::Providers.define_provider_methods(self) do |feed, method, opts, &block|
@@ -14,19 +20,21 @@ module SimpleFeed
14
20
  end
15
21
 
16
22
  def initialize(name)
17
- @name = name
18
- @name = name.underscore.to_sym unless name.is_a?(Symbol)
23
+ @name = name
24
+ @name = name.underscore.to_sym unless name.is_a?(Symbol)
19
25
  # set the defaults if not passed in
20
- @meta = {}
21
- @namespace = nil
26
+ @meta = {}
27
+ @namespace = nil
22
28
  @per_page ||= 50
23
29
  @max_size ||= 1000
24
30
  @batch_size ||= 10
31
+ @meta_key_transformer = nil
32
+ @data_key_transformer = nil
25
33
  @proxy = nil
26
34
  end
27
35
 
28
36
  def provider=(definition)
29
- @proxy = Providers::Proxy.from(definition)
37
+ @proxy = Providers::Proxy.from(definition)
30
38
  @proxy.feed = self
31
39
  @proxy
32
40
  end
@@ -63,12 +71,15 @@ module SimpleFeed
63
71
  end
64
72
 
65
73
  def key(user_id)
66
- SimpleFeed::Providers::Key.new(user_id, key_template)
74
+ SimpleFeed::Providers::Key.new(user_id,
75
+ namespace: namespace,
76
+ data_key_transformer: data_key_transformer,
77
+ meta_key_transformer: meta_key_transformer)
67
78
  end
68
79
 
69
80
  def eql?(other)
70
81
  other.class == self.class &&
71
- %i(per_page max_size name).all? { |m| send(m).equal?(other.send(m)) } &&
82
+ %i(per_page max_size name namespace data_key_transformer meta_key_transformer).all? { |m| send(m).equal?(other.send(m)) } &&
72
83
  provider.provider.class == other.provider.provider.class
73
84
  end
74
85
 
@@ -58,7 +58,7 @@ module SimpleFeed
58
58
  def with_response_batched(user_ids, external_response = nil)
59
59
  with_response(external_response) do |response|
60
60
  batch(user_ids) do |key|
61
- response.for(key.user_id) { yield(key, response) }
61
+ response.for(key.consumer) { yield(key, response) }
62
62
  end
63
63
  end
64
64
  end
@@ -2,10 +2,6 @@
2
2
 
3
3
  require 'base62-rb'
4
4
  require 'hashie/mash'
5
- require 'simplefeed/key/template'
6
- require 'simplefeed/key/type'
7
-
8
- require 'forwardable'
9
5
 
10
6
  module SimpleFeed
11
7
  module Providers
@@ -15,66 +11,85 @@ module SimpleFeed
15
11
  # ↓ ↓
16
12
  # "ff|u.f23098.m"
17
13
  # ↑ ↑
18
- # namespace user_id(base62)
14
+ # namespace consumer(base62)
19
15
  #
20
16
  class Key
21
- attr_accessor :user_id, :key_template
17
+ class << self
18
+ def rot13(value)
19
+ value.tr('abcdefghijklmnopqrstuvwxyz',
20
+ 'nopqrstuvwxyzabcdefghijklm')
21
+ end
22
+ end
22
23
 
23
- extend Forwardable
24
- def_delegators :@key_template, :key_names, :key_types
24
+ SERIALIZED_DATA_TEMPLATE = '{{namespace}}u.{{data_id}}.d'
25
+ SERIALIZED_META_TEMPLATE = '{{namespace}}u.{{meta_id}}.m'
25
26
 
26
- def initialize(user_id, key_template)
27
- self.user_id = user_id
28
- self.key_template = key_template
27
+ attr_reader :consumer, :namespace, :data_key_transformer, :meta_key_transformer
29
28
 
30
- define_key_methods
29
+ def initialize(consumer,
30
+ namespace: nil,
31
+ data_key_transformer: nil,
32
+ meta_key_transformer: nil)
33
+ @consumer = consumer
34
+ @namespace = namespace
35
+ @data_key_transformer = data_key_transformer
36
+ @meta_key_transformer = meta_key_transformer
31
37
  end
32
38
 
33
- # Defines #data and #meta methods.
34
- def define_key_methods
35
- key_template.key_types.each do |type|
36
- key_name = type.name
37
- next if respond_to?(key_name)
38
-
39
- self.class.send(:define_method, key_name) do
40
- instance_variable_get("@#{key_name}") ||
41
- instance_variable_set("@#{key_name}", type.render(render_options))
42
- end
43
- end
39
+ def data
40
+ @data ||= render(SERIALIZED_DATA_TEMPLATE)
44
41
  end
45
42
 
46
- def base62_user_id
47
- @base62_user_id ||= if user_id.is_a?(Numeric)
48
- ::Base62.encode(user_id)
49
- else
50
- rot13(user_id.to_s)
51
- end
43
+ def meta
44
+ @meta ||= render(SERIALIZED_META_TEMPLATE)
52
45
  end
53
46
 
54
47
  def keys
55
- key_names.map { |name| send(name) }
56
- end
57
-
58
- def render_options
59
- key_template.render_options.merge!({
60
- 'user_id' => user_id,
61
- 'base62_user_id' => base62_user_id
62
- })
48
+ [data, meta]
63
49
  end
64
50
 
65
51
  def to_s
66
- super + { user_id: user_id, base62_user_id: base62_user_id, keys: keys }.to_s
52
+ super + key_params.to_s
67
53
  end
68
54
 
69
55
  def inspect
70
- render_options.inspect
56
+ super + key_params.inspect
71
57
  end
72
58
 
73
59
  private
74
60
 
75
- def rot13(value)
76
- value.tr('abcdefghijklmnopqrstuvwxyz',
77
- 'nopqrstuvwxyzabcdefghijklm')
61
+ def render(template)
62
+ template.dup.tap do |output|
63
+ key_params.each_pair do |key, value|
64
+ output.gsub!(/{{#{key}}}/, value.to_s)
65
+ end
66
+ end
67
+ end
68
+
69
+ def obscure_value(id)
70
+ id = id.to_i if id.is_a?(String) && id =~ /^\d+$/
71
+
72
+ if id.is_a?(Numeric)
73
+ ::Base62.encode(id)
74
+ else
75
+ self.class.rot13(id.to_s)
76
+ end
77
+ end
78
+
79
+ def key_params
80
+ @key_params ||= Hashie::Mash.new(
81
+ namespace: namespace ? "#{namespace}|" : '',
82
+ data_id: obscure_value(data_id),
83
+ meta_id: obscure_value(meta_id)
84
+ )
85
+ end
86
+
87
+ def meta_id
88
+ meta_key_transformer&.call(consumer) || consumer
89
+ end
90
+
91
+ def data_id
92
+ data_key_transformer&.call(consumer) || consumer
78
93
  end
79
94
  end
80
95
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SimpleFeed
4
4
  module Providers
5
+ RUBY_MAJOR_VERSION = RUBY_VERSION.split('.')[0..1].join.to_i
6
+
5
7
  class Proxy
6
8
  attr_accessor :provider
7
9
 
@@ -15,7 +17,7 @@ module SimpleFeed
15
17
  end
16
18
 
17
19
  def initialize(provider_or_klass, *args, **options)
18
- self.provider = if provider_or_klass.is_a?(::String)
20
+ self.provider = if provider_or_klass.is_a?(::String) || provider_or_klass.is_a?(::Symbol)
19
21
  ::Object.const_get(provider_or_klass).new(*args, **options)
20
22
  else
21
23
  provider_or_klass
@@ -26,12 +28,23 @@ module SimpleFeed
26
28
  end
27
29
  end
28
30
 
29
- # Forward all other method calls to Provider
30
- def method_missing(name, *args, **opts, &block)
31
- if provider&.respond_to?(name)
32
- provider.send(name, *args, **opts, &block)
33
- else
34
- super(name, *args, **opts, &block)
31
+ if RUBY_MAJOR_VERSION >= 27
32
+ # Forward all other method calls to Provider
33
+ def method_missing(name, *args, **opts, &block)
34
+ if provider&.respond_to?(name)
35
+ provider.send(name, *args, **opts, &block)
36
+ else
37
+ super(name, *args, **opts, &block)
38
+ end
39
+ end
40
+ else
41
+ # Forward all other method calls to Provider
42
+ def method_missing(name, *args, &block)
43
+ if provider&.respond_to?(name)
44
+ provider.send(name, *args, &block)
45
+ else
46
+ super(name, *args, &block)
47
+ end
35
48
  end
36
49
  end
37
50
  end
@@ -139,12 +139,10 @@ module SimpleFeed
139
139
  end
140
140
  end
141
141
 
142
- def with_pipelined
142
+ def with_pipelined(&block)
143
143
  with_retries do
144
144
  with_redis do |redis|
145
- redis.pipelined do
146
- yield(redis)
147
- end
145
+ redis.pipelined(&block)
148
146
  end
149
147
  end
150
148
  end
@@ -51,9 +51,9 @@ module SimpleFeed
51
51
  raise ArgumentError, '#delete_if must be called with a block that receives (user_id, event) as arguments.' unless block_given?
52
52
 
53
53
  with_response_batched(user_ids) do |key|
54
- fetch(user_ids: [key.user_id])[key.user_id].map do |event|
54
+ fetch(user_ids: [key.consumer])[key.consumer].map do |event|
55
55
  with_redis do |redis|
56
- if yield(event, key.user_id)
56
+ if yield(event, key.consumer)
57
57
  redis.zrem(key.data, event.value) ? event : nil
58
58
  end
59
59
  end
@@ -90,7 +90,7 @@ module SimpleFeed
90
90
 
91
91
  response = with_response_pipelined(user_ids) do |redis, key|
92
92
  if since == :unread
93
- redis.zrevrangebyscore(key.data, '+inf', (last_read_response.delete(key.user_id) || 0).to_f, withscores: true)
93
+ redis.zrevrangebyscore(key.data, '+inf', (last_read_response.delete(key.consumer) || 0).to_f, withscores: true)
94
94
  elsif since
95
95
  redis.zrevrangebyscore(key.data, '+inf', since.to_f, withscores: true)
96
96
  else
@@ -120,7 +120,7 @@ module SimpleFeed
120
120
  get_users_last_read(redis, key)
121
121
  end
122
122
  with_response_pipelined(response.user_ids, response) do |redis, key, _response|
123
- last_read = _response.delete(key.user_id).to_f
123
+ last_read = _response.delete(key.consumer).to_f
124
124
  redis.zcount(key.data, last_read, '+inf')
125
125
  end
126
126
  end
@@ -223,7 +223,7 @@ module SimpleFeed
223
223
  def with_response_pipelined(user_ids, response = nil)
224
224
  with_response(response) do |response|
225
225
  batch_pipelined(user_ids) do |redis, key|
226
- response.for(key.user_id) { yield(redis, key, response) }
226
+ response.for(key.consumer) { yield(redis, key, response) }
227
227
  end
228
228
  end
229
229
  end
@@ -231,7 +231,7 @@ module SimpleFeed
231
231
  def with_response_multi(user_ids, response = nil)
232
232
  with_response(response) do |response|
233
233
  batch_multi(user_ids) do |redis, key|
234
- response.for(key.user_id) { yield(redis, key, response) }
234
+ response.for(key.consumer) { yield(redis, key, response) }
235
235
  end
236
236
  end
237
237
  end
@@ -52,5 +52,4 @@ module SimpleFeed
52
52
  end
53
53
  end
54
54
 
55
- require_relative 'providers/hash'
56
55
  require_relative 'providers/redis'
@@ -12,11 +12,9 @@ module SimpleFeed
12
12
 
13
13
  include Enumerable
14
14
 
15
- def each
15
+ def each(&block)
16
16
  if block_given?
17
- @result.each_pair do |user_id, result|
18
- yield(user_id, result)
19
- end
17
+ @result.each_pair(&block)
20
18
  else
21
19
  @result.keys.to_enum
22
20
  end
@@ -28,7 +26,7 @@ module SimpleFeed
28
26
 
29
27
  def for(key_or_user_id, result = nil)
30
28
  user_id = key_or_user_id.is_a?(SimpleFeed::Providers::Key) ?
31
- key_or_user_id.user_id :
29
+ key_or_user_id.consumer :
32
30
  key_or_user_id
33
31
 
34
32
  @result[user_id] = result || yield(@result[user_id])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleFeed
4
- VERSION = '2.1.0'
4
+ VERSION ||= '3.1.2'
5
5
  end