browsing_history 0.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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +89 -0
  5. data/.rubocop_todo.yml +28 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +180 -0
  9. data/LICENSE +21 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +2 -0
  12. data/README.rdoc +3 -0
  13. data/Rakefile +28 -0
  14. data/browsing_history.gemspec +34 -0
  15. data/dump.rdb +1 -0
  16. data/lib/browsing_history/browser.rb +66 -0
  17. data/lib/browsing_history/configuration.rb +38 -0
  18. data/lib/browsing_history/error.rb +2 -0
  19. data/lib/browsing_history/historizable.rb +26 -0
  20. data/lib/browsing_history/history.rb +85 -0
  21. data/lib/browsing_history/storage.rb +44 -0
  22. data/lib/browsing_history/storages/active_record.rb +12 -0
  23. data/lib/browsing_history/storages/base.rb +40 -0
  24. data/lib/browsing_history/storages/redis.rb +113 -0
  25. data/lib/browsing_history/version.rb +3 -0
  26. data/lib/browsing_history.rb +12 -0
  27. data/lib/generators/active_record/migration_generator.rb +12 -0
  28. data/lib/generators/active_record/templates/migration.rb +15 -0
  29. data/lib/tasks/browsing_history_tasks.rake +4 -0
  30. data/spec/browsing_history/browser_spec.rb +156 -0
  31. data/spec/browsing_history/configration_spec.rb +16 -0
  32. data/spec/browsing_history/fixtures/article.rb +10 -0
  33. data/spec/browsing_history/fixtures/user.rb +10 -0
  34. data/spec/browsing_history/historizable_spec.rb +27 -0
  35. data/spec/browsing_history/history_spec.rb +273 -0
  36. data/spec/browsing_history/storage_spec.rb +45 -0
  37. data/spec/browsing_history/storages/base_spec.rb +35 -0
  38. data/spec/browsing_history/storages/redis_spec.rb +141 -0
  39. data/spec/browsing_history_spec.rb +4 -0
  40. data/spec/generator_spec.rb +34 -0
  41. data/spec/spec_helper.rb +40 -0
  42. data/spec/test_app/README.rdoc +28 -0
  43. data/spec/test_app/Rakefile +6 -0
  44. data/spec/test_app/app/assets/images/.keep +0 -0
  45. data/spec/test_app/app/assets/javascripts/application.js +13 -0
  46. data/spec/test_app/app/assets/stylesheets/application.css +15 -0
  47. data/spec/test_app/app/controllers/application_controller.rb +10 -0
  48. data/spec/test_app/app/controllers/articles_controller.rb +17 -0
  49. data/spec/test_app/app/controllers/concerns/.keep +0 -0
  50. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  51. data/spec/test_app/app/mailers/.keep +0 -0
  52. data/spec/test_app/app/models/.keep +0 -0
  53. data/spec/test_app/app/models/concerns/.keep +0 -0
  54. data/spec/test_app/app/views/layouts/application.html.erb +14 -0
  55. data/spec/test_app/bin/bundle +3 -0
  56. data/spec/test_app/bin/rails +4 -0
  57. data/spec/test_app/bin/rake +4 -0
  58. data/spec/test_app/bin/setup +29 -0
  59. data/spec/test_app/config/application.rb +19 -0
  60. data/spec/test_app/config/boot.rb +5 -0
  61. data/spec/test_app/config/database.yml +12 -0
  62. data/spec/test_app/config/environment.rb +5 -0
  63. data/spec/test_app/config/environments/development.rb +18 -0
  64. data/spec/test_app/config/environments/test.rb +21 -0
  65. data/spec/test_app/config/initializers/assets.rb +11 -0
  66. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  68. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  69. data/spec/test_app/config/initializers/inflections.rb +16 -0
  70. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  71. data/spec/test_app/config/initializers/session_store.rb +3 -0
  72. data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
  73. data/spec/test_app/config/locales/en.yml +23 -0
  74. data/spec/test_app/config/routes.rb +2 -0
  75. data/spec/test_app/config/secrets.yml +5 -0
  76. data/spec/test_app/config.ru +4 -0
  77. data/spec/test_app/db/development.sqlite3 +0 -0
  78. data/spec/test_app/db/test.sqlite3 +0 -0
  79. data/spec/test_app/lib/assets/.keep +0 -0
  80. data/spec/test_app/log/.keep +0 -0
  81. data/spec/test_app/log/development.log +0 -0
  82. data/spec/test_app/log/test.log +0 -0
  83. data/spec/test_app/public/404.html +67 -0
  84. data/spec/test_app/public/422.html +67 -0
  85. data/spec/test_app/public/500.html +66 -0
  86. data/spec/test_app/public/favicon.ico +0 -0
  87. metadata +342 -0
@@ -0,0 +1,44 @@
1
+ require 'browsing_history/storages/base'
2
+
3
+ module BrowsingHistory::Storage
4
+ class InvalidStorage < BrowsingHistory::Error; end
5
+ class StoragesNotSet < BrowsingHistory::Error; end
6
+
7
+ def self.included(klass)
8
+ klass.extend ConfigureMethods
9
+ klass.init_storages
10
+ end
11
+
12
+ module ConfigureMethods
13
+ attr_reader :current_storage_type
14
+
15
+ def current_storage
16
+ storages[current_storage_type]
17
+ end
18
+
19
+ def storages
20
+ @storages ||= {}
21
+ end
22
+
23
+ def attach_storage(storage_type, **opts)
24
+ storage = storages[storage_type]
25
+
26
+ if storage
27
+ storage.connect(opts)
28
+ @current_storage_type = storage_type
29
+ else
30
+ raise InvalidStorage
31
+ end
32
+ end
33
+
34
+ def init_storages
35
+ BrowsingHistory.storage_types.each do |key|
36
+ storages[key] =
37
+ "BrowsingHistory::Storages::#{key.to_s.camelize}".constantize
38
+ end
39
+
40
+ raise StoragesNotSet if storages.empty?
41
+ attach_storage(storages.keys.first)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_record'
2
+
3
+ class BrowsingHistory::Storages::ActiveRecord < BrowsingHistory::Storages::Base
4
+ class << self
5
+ def connect(_opts = {})
6
+ self
7
+ end
8
+
9
+ def default_connect_option
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ module BrowsingHistory::Storages
2
+ IOMethods = %i(fetch add update clear count interface).freeze
3
+
4
+ class NotOverideError < BrowsingHistory::Error; end
5
+
6
+ class Base
7
+ attr_accessor :browser, :historizable, :options
8
+
9
+ def initialize(browser, historizable)
10
+ @browser = browser
11
+ @historizable = historizable
12
+ end
13
+
14
+ def method_missing(method, *args)
15
+ if method.in?(IOMethods)
16
+ raise NotOverideError
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def connect
24
+ IOMethods.each do |method|
25
+ define_singleton_method(method) do |browser, historizable, *_args|
26
+ opts = _args.dup.extract_options!.merge(default_options)
27
+ new(browser, historizable).send(method, **opts)
28
+ end
29
+ end
30
+ end
31
+
32
+ def default_options
33
+ {}
34
+ end
35
+ end
36
+ end
37
+
38
+ require 'browsing_history/storages/redis'
39
+ require 'browsing_history/storages/active_record'
40
+ end
@@ -0,0 +1,113 @@
1
+ require 'redis'
2
+ require 'redis-objects'
3
+ require 'redis-namespace'
4
+
5
+ class BrowsingHistory::Storages::Redis < BrowsingHistory::Storages::Base
6
+ def fetch(opts)
7
+ if opts[:between]
8
+ fetch_between(**opts)
9
+ elsif opts[:at]
10
+ history.at(opts[:at])
11
+ else
12
+ limit = opts[:limit].try(:-, 1) || opts.delete(:end)
13
+ history.revrange(opts.delete(:start), limit, **opts)
14
+ end
15
+ end
16
+
17
+ def add(_opts)
18
+ history.add(historizable, score)
19
+ end
20
+
21
+ def update(opts)
22
+ end
23
+
24
+ def clear(opts)
25
+ if opts[:expiration]
26
+ clear_expiration(opts)
27
+ elsif opts[:all]
28
+ clear_all
29
+ else
30
+ history.delete(historizable)
31
+ end
32
+ end
33
+
34
+ def count(opts)
35
+ if opts[:between]
36
+ count_between(opts)
37
+ else
38
+ history.range_size(
39
+ history.score(history.first),
40
+ history.score(history.last)
41
+ )
42
+ end
43
+ end
44
+
45
+ def interface(_opts)
46
+ history
47
+ end
48
+
49
+ private
50
+
51
+ def history
52
+ key = %(\
53
+ #{browser.browser_type}/\
54
+ #{browser.browser_id}/\
55
+ #{historizable.historizable_type}\
56
+ ).delete(' ')
57
+
58
+ self.class.set.new(key, marshal: true)
59
+ end
60
+
61
+ def fetch_between(opts)
62
+ start_date, end_date = parse_between(opts.delete(:between))
63
+
64
+ history.revrangebyscore(start_date, end_date, **opts)
65
+ end
66
+
67
+ def clear_expiration(opts)
68
+ min_score = history.score(history.first)
69
+
70
+ history.remrangebyscore(min_score, score(opts[:expiration]))
71
+ end
72
+
73
+ def clear_all
74
+ history.remrangebyrank(0, -1)
75
+ end
76
+
77
+ def count_between(opts)
78
+ end_date, start_date = parse_between(opts.delete(:between))
79
+
80
+ history.range_size(start_date, end_date)
81
+ end
82
+
83
+ def score(time = nil)
84
+ time ? time.to_f : Time.now.to_f
85
+ end
86
+
87
+ def parse_between(range)
88
+ [range.first.to_f, range.last.to_f]
89
+ end
90
+
91
+ class << self
92
+ attr_reader :redis, :set
93
+
94
+ def connect(opts = {})
95
+ @redis = Redis::Namespace.new(
96
+ BrowsingHistory.namespace,
97
+ redis: Redis.new(default_connect_options.merge(opts))
98
+ )
99
+ Redis::Objects.redis = @redis
100
+ @set = Redis::SortedSet
101
+
102
+ super()
103
+ end
104
+
105
+ def default_options
106
+ { start: 0, end: -1 }
107
+ end
108
+
109
+ def default_connect_options
110
+ { host: '127.0.0.1', port: 6379, db: 0 }
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module BrowsingHistory
2
+ VERSION = '0.0.2'.freeze
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+
4
+ module BrowsingHistory
5
+ require 'browsing_history/error'
6
+ require 'browsing_history/configuration'
7
+ extend Configuration
8
+
9
+ require 'browsing_history/history'
10
+ require 'browsing_history/browser'
11
+ require 'browsing_history/historizable'
12
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/actions'
3
+ require 'rails/generators/active_record'
4
+
5
+ class BrowsingHistory::MigrationGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ argument :name, type: :string, default: 'browsing_histories'
9
+ def generate_files
10
+ migration_template 'migration.rb', "db/migrate/create_#{name}.rb"
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateBrowsingHistories < ActiveRecord::Migration
2
+ # Create table
3
+ def up
4
+ create_table :browsing_histories do |t|
5
+ t.references :browser, polymorphic: true, index: true
6
+ t.references :historizable, polymorphic: true, index: true
7
+ t.text :object, null: false
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+
12
+ # Drop table
13
+ def down
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :browsing_history do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+ require 'fixtures/user'
3
+ require 'fixtures/article'
4
+
5
+ describe BrowsingHistory::Browser do
6
+ articles_size = 50
7
+
8
+ let(:user) { User.new }
9
+ let(:article) { Article.new }
10
+ let(:articles) do
11
+ articles_size.times.map.with_index(1) { |i| Article.new(i) }
12
+ end
13
+ let(:histories) { user.browsing_histories }
14
+
15
+ describe 'respond_to' do
16
+ context 'instance' do
17
+ subject { user }
18
+
19
+ it { is_expected.to respond_to(:browsing_histories) }
20
+ it { is_expected.to respond_to(:browser_id) }
21
+ it { is_expected.to respond_to(:browser_type) }
22
+ it { is_expected.to respond_to(:browser_class) }
23
+ it { is_expected.to respond_to(:browser_instance?) }
24
+ end
25
+
26
+ context 'singleton' do
27
+ subject { user.class }
28
+
29
+ it { is_expected.to respond_to(:browser_id) }
30
+ it { is_expected.to respond_to(:browser_type) }
31
+ it { is_expected.to respond_to(:browser_class) }
32
+ it { is_expected.to respond_to(:browser_instance?) }
33
+ end
34
+ end
35
+
36
+ describe 'assosiation' do
37
+ describe 'respond_to' do
38
+ subject { user.browsing_histories }
39
+
40
+ it { is_expected.to respond_to(:recent) }
41
+ it { is_expected.to respond_to(:previous) }
42
+ it { is_expected.to respond_to(:add) }
43
+ it { is_expected.to respond_to(:<<) }
44
+ it { is_expected.to respond_to(:count) }
45
+ end
46
+
47
+ describe '#initialize' do
48
+ subject { user.browsing_histories }
49
+ it { is_expected.to be_a("#{described_class}::Association".constantize) }
50
+ end
51
+
52
+ describe '#recent' do
53
+ context 'default' do
54
+ it 'should default empty' do
55
+ expect(histories.recent(article)).to be_empty
56
+ end
57
+ end
58
+
59
+ context 'when histories exist' do
60
+ before(:each) do
61
+ articles.each { |article| histories << article }
62
+ end
63
+ let(:recent_articles) { histories.recent(article, limit: articles.size) }
64
+
65
+ it 'should equal original' do
66
+ recent_articles.each_with_index do |article, i|
67
+ original = articles.reverse[i]
68
+
69
+ expect(article.id).to eq(original.id)
70
+ expect(article.title).to eq(original.title)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '#previous' do
77
+ context 'default' do
78
+ it 'should default empty' do
79
+ expect(histories.previous(article, Time.now..1.day.ago)).to be_empty
80
+ end
81
+ end
82
+
83
+ context 'when histories exist' do
84
+ before do
85
+ articles.each { |article| histories << article }
86
+ end
87
+
88
+ let(:recent_article) { Article.new(100) }
89
+ let(:previous_article) do
90
+ histories.previous(article, Time.now..5.seconds.ago)
91
+ end
92
+
93
+ it 'should return 1 historizable' do
94
+ sleep(6)
95
+ histories << recent_article
96
+
97
+ expect(previous_article.size).to eq(1)
98
+ expect(previous_article.first.id).to eq(recent_article.id)
99
+ expect(previous_article.first.title).to eq(recent_article.title)
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#add' do
105
+ context 'add' do
106
+ it 'should return historizable' do
107
+ expect(histories.add(article)).to be_truthy
108
+ end
109
+
110
+ context 'before add historizable' do
111
+ before { histories.add(article) }
112
+
113
+ let(:saved_article) { histories.recent(article).first }
114
+ it 'should equal original' do
115
+ expect(saved_article.id).to eq(article.id)
116
+ expect(saved_article.title).to eq(article.title)
117
+ end
118
+ end
119
+ end
120
+
121
+ context '<<' do
122
+ it 'should return historizable' do
123
+ expect(histories << article).to be_truthy
124
+ end
125
+
126
+ context 'before << historizable' do
127
+ before { histories << article }
128
+
129
+ let(:saved_article) { histories.recent(article).first }
130
+ it 'should equal original' do
131
+ expect(saved_article.id).to eq(article.id)
132
+ expect(saved_article.title).to eq(article.title)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#count' do
138
+ context 'default' do
139
+ it 'should default empty' do
140
+ expect(histories.count(article)).to eq(0)
141
+ end
142
+ end
143
+
144
+ context 'when histories exist' do
145
+ before do
146
+ articles.each { |article| histories << article }
147
+ end
148
+
149
+ it 'should return historizables count' do
150
+ expect(histories.count(article)).to eq(articles.size)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe BrowsingHistory::Configuration do
4
+ let(:storage_types) { %i(redis active_record) }
5
+ let(:storage_type) { storage_types.first }
6
+
7
+ describe '#configure' do
8
+ it 'should have default attributes' do
9
+ BrowsingHistory.configure do |configuration|
10
+ expect(configuration.storage_types).to eql(storage_types)
11
+ expect(configuration.storage_type).to eql(storage_type)
12
+ expect(configuration.namespace).to eql('browsing_history')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class Article
2
+ include BrowsingHistory::Historizable
3
+
4
+ attr_reader :id, :title
5
+
6
+ def initialize(id = nil)
7
+ @id = id || rand(1000)
8
+ @title = random_string
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class User
2
+ include BrowsingHistory::Browser
3
+
4
+ attr_reader :id, :title
5
+
6
+ def initialize
7
+ @id = rand(1000)
8
+ @name = random_string
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'fixtures/user'
3
+ require 'fixtures/article'
4
+
5
+ describe BrowsingHistory::Browser do
6
+ let(:article) { Article.new }
7
+
8
+ describe 'respond_to' do
9
+ context 'instance' do
10
+ subject { article }
11
+
12
+ it { is_expected.to respond_to(:historizable_id) }
13
+ it { is_expected.to respond_to(:historizable_type) }
14
+ it { is_expected.to respond_to(:historizable_class) }
15
+ it { is_expected.to respond_to(:historizable_instance?) }
16
+ end
17
+
18
+ context 'singleton' do
19
+ subject { article.class }
20
+
21
+ it { is_expected.to respond_to(:historizable_id) }
22
+ it { is_expected.to respond_to(:historizable_type) }
23
+ it { is_expected.to respond_to(:historizable_class) }
24
+ it { is_expected.to respond_to(:historizable_instance?) }
25
+ end
26
+ end
27
+ end