browsing_history 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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