redcrumbs 0.5.0 → 0.5.1
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 +15 -0
- data/.gitignore +1 -0
- data/.travis.yml +33 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +140 -105
- data/gemfiles/Gemfile.rails-3.1.x +12 -0
- data/gemfiles/Gemfile.rails-3.2.x +12 -0
- data/gemfiles/Gemfile.rails-4.0.x +12 -0
- data/gemfiles/Gemfile.rails-4.1.x +12 -0
- data/lib/generators/redcrumbs/templates/initializer.rb +9 -26
- data/lib/redcrumbs.rb +12 -7
- data/lib/redcrumbs/config.rb +55 -5
- data/lib/redcrumbs/creation.rb +60 -47
- data/lib/redcrumbs/crumb.rb +100 -0
- data/lib/redcrumbs/options.rb +8 -3
- data/lib/redcrumbs/serializable_association.rb +193 -0
- data/lib/redcrumbs/users.rb +27 -37
- data/lib/redcrumbs/version.rb +1 -1
- data/redcrumbs.gemspec +11 -9
- data/spec/redcrumbs/config_spec.rb +168 -0
- data/spec/redcrumbs/creation_spec.rb +271 -0
- data/spec/redcrumbs/crumb_spec.rb +254 -0
- data/spec/redcrumbs/options_spec.rb +70 -0
- data/spec/redcrumbs/serializable_association_spec.rb +101 -0
- data/spec/redcrumbs/users_spec.rb +55 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/models.rb +34 -0
- data/spec/support/schema.rb +26 -0
- metadata +106 -34
- data/app/models/redcrumbs/crumb.rb +0 -68
- data/app/models/redcrumbs/crumb/expiry.rb +0 -23
- data/app/models/redcrumbs/crumb/getters.rb +0 -74
- data/app/models/redcrumbs/crumb/setters.rb +0 -28
- data/lib/redcrumbs/engine.rb +0 -8
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activerecord', '~> 4.0'
|
4
|
+
gem 'activesupport', '~> 4.0'
|
5
|
+
|
6
|
+
gemspec :path => '..'
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'codeclimate-test-reporter', :group => :test, :require => nil
|
10
|
+
gem 'rspec', '~> 3.0'
|
11
|
+
gem 'sqlite3', '~> 1.0'
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activerecord', '~> 4.1'
|
4
|
+
gem 'activesupport', '~> 4.1'
|
5
|
+
|
6
|
+
gemspec :path => '..'
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'codeclimate-test-reporter', :group => :test, :require => nil
|
10
|
+
gem 'rspec', '~> 3.0'
|
11
|
+
gem 'sqlite3', '~> 1.0'
|
12
|
+
end
|
@@ -1,34 +1,17 @@
|
|
1
1
|
Redcrumbs.setup do |config|
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
2
|
+
# If your activity feeds are user-based you can store creator and target
|
3
|
+
# attributes on the crumb object to avoid having to touch your main database
|
4
|
+
# at all. Keep it sensible and evaluate whether the additional space used in
|
5
|
+
# Redis is really worth the time saving.
|
6
|
+
# Note: You don't need to store the object id, it is already stored.
|
5
7
|
#
|
6
|
-
# config.
|
7
|
-
# config.
|
8
|
-
# config.target_class_sym = :user
|
9
|
-
# config.target_primary_key = 'id'
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# If you're using the crumbs to report news back to a user you can store creator and target
|
13
|
-
# attributes on the crumb object to avoid having to touch your main database at all. Keep it
|
14
|
-
# sensible and evaluate whether the additional space used in Redis is really worth the time saving.
|
15
|
-
#
|
16
|
-
# config.store_creator_attributes = [:id, :name, :email]
|
17
|
-
# config.store_target_attributes = [:id, :name, :email]
|
8
|
+
# config.store_creator_attributes = [:name, :email]
|
9
|
+
# config.store_target_attributes = [:name, :email]
|
18
10
|
#
|
19
11
|
#
|
20
12
|
# Set the mortality to make crumbs automatically expire in time. Default is infinity.
|
21
13
|
# config.mortality = 30.days
|
22
14
|
end
|
23
15
|
|
24
|
-
# Point this to your redis connection. It can be a Redis
|
25
|
-
Redcrumbs.redis = 'localhost:6379'
|
26
|
-
|
27
|
-
# You may want to create a config/redcrumbs.yml file and base your redis setting
|
28
|
-
# from that. e.g. (thanks to Resque for this):
|
29
|
-
|
30
|
-
# rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..'
|
31
|
-
# rails_env = ENV['RAILS_ENV'] || 'development'
|
32
|
-
#
|
33
|
-
# redcrumbs_config = YAML.load_file(rails_root + '/config/redcrumbs.yml')
|
34
|
-
# Redcrumbs.redis = redcrumbs_config[rails_env]
|
16
|
+
# Point this to your redis connection. It can be a Redis client, namespace or a URL string.
|
17
|
+
Redcrumbs.redis = 'localhost:6379'
|
data/lib/redcrumbs.rb
CHANGED
@@ -1,24 +1,28 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
1
3
|
require 'active_support/concern'
|
2
4
|
require 'active_support/dependencies/autoload'
|
3
|
-
require
|
4
|
-
require 'redcrumbs/
|
5
|
+
require 'active_record'
|
6
|
+
require 'redcrumbs/version'
|
5
7
|
require 'redcrumbs/config'
|
6
8
|
require 'redis'
|
7
9
|
require 'redis-namespace'
|
8
10
|
require 'dm-core'
|
9
11
|
|
10
|
-
# Redcrumbs
|
12
|
+
# Redcrumbs uses `dirty attributes` to track and store changes to ActiveRecord models in a way that is fast and
|
11
13
|
# unobtrusive. By storing the data in Redis instead of a SQL database the footprint is greatly reduced, no
|
12
14
|
# schema changes are necessary and we can harness all the advantages of a key value store; such as key expiry.
|
13
15
|
#
|
14
16
|
# Author:: John Hope
|
15
|
-
# Copyright:: Copyright (c)
|
17
|
+
# Copyright:: Copyright (c) 2014 John Hope for Project Zebra
|
16
18
|
# License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
|
17
19
|
#
|
18
20
|
# To start tracking a model use the 'redcrumbed' method:
|
19
21
|
#
|
20
22
|
# class Venue
|
21
23
|
# redcrumbed, :only => [:name, :latlng]
|
24
|
+
#
|
25
|
+
# has_one :creator, :class_name => 'User'
|
22
26
|
# end
|
23
27
|
#
|
24
28
|
# venue = Venue.last
|
@@ -38,6 +42,10 @@ module Redcrumbs
|
|
38
42
|
autoload :Options
|
39
43
|
autoload :Users
|
40
44
|
autoload :Creation
|
45
|
+
autoload :Crumb
|
46
|
+
|
47
|
+
include Options
|
48
|
+
include Users
|
41
49
|
|
42
50
|
def self.setup
|
43
51
|
yield self
|
@@ -49,9 +57,6 @@ module Redcrumbs
|
|
49
57
|
|
50
58
|
module ClassMethods
|
51
59
|
def redcrumbed(options = {})
|
52
|
-
|
53
|
-
include Options
|
54
|
-
include Users
|
55
60
|
include Creation
|
56
61
|
|
57
62
|
prepare_redcrumbed_options(options)
|
data/lib/redcrumbs/config.rb
CHANGED
@@ -10,14 +10,32 @@ module Redcrumbs
|
|
10
10
|
mattr_accessor :mortality
|
11
11
|
mattr_accessor :redis
|
12
12
|
|
13
|
+
mattr_accessor :class_name
|
14
|
+
|
15
|
+
|
16
|
+
# This should only be used to load old crumbs from previous versions
|
17
|
+
# of the gem, in future require an explicit creator/target method to set.
|
18
|
+
#
|
13
19
|
@@creator_class_sym ||= :user
|
14
20
|
@@creator_primary_key ||= 'id'
|
15
|
-
@@target_class_sym ||= :user
|
21
|
+
@@target_class_sym ||= :user
|
16
22
|
@@target_primary_key ||= 'id'
|
17
23
|
|
18
24
|
@@store_creator_attributes ||= []
|
19
25
|
@@store_target_attributes ||= []
|
26
|
+
|
27
|
+
|
28
|
+
# Constantises the class_name attribute, falls back to the Crumb default.
|
29
|
+
#
|
30
|
+
def self.crumb_class
|
31
|
+
if @@class_name and @@class_name.length > 0
|
32
|
+
constantize_class_name
|
33
|
+
else
|
34
|
+
Crumb
|
35
|
+
end
|
36
|
+
end
|
20
37
|
|
38
|
+
|
21
39
|
# Stolen from resque. Thanks!
|
22
40
|
# Accepts:
|
23
41
|
# 1. A 'hostname:port' String
|
@@ -26,6 +44,7 @@ module Redcrumbs
|
|
26
44
|
# 4. A Redis URL String 'redis://host:port'
|
27
45
|
# 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
|
28
46
|
# or `Redis::Namespace`.
|
47
|
+
#
|
29
48
|
def self.redis=(server)
|
30
49
|
case server
|
31
50
|
when String
|
@@ -45,12 +64,43 @@ module Redcrumbs
|
|
45
64
|
else
|
46
65
|
@@redis = Redis::Namespace.new(:redcrumbs, :redis => server)
|
47
66
|
end
|
67
|
+
|
68
|
+
setup_datamapper!
|
69
|
+
|
48
70
|
@@redis
|
49
71
|
end
|
50
72
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
73
|
+
private
|
74
|
+
|
75
|
+
# Note: Since it's not possible to access the exact connection the DataMapper adapter
|
76
|
+
# uses we have to use the @@redis module variable and make sure it's consistent.
|
77
|
+
#
|
78
|
+
def self.setup_datamapper!
|
79
|
+
adapter = DataMapper.setup(:default,
|
80
|
+
{ :adapter => "redis",
|
81
|
+
:host => self.redis.client.host,
|
82
|
+
:port => self.redis.client.port,
|
83
|
+
:password => self.redis.client.password
|
84
|
+
})
|
85
|
+
|
86
|
+
# For supporting namespaces:
|
87
|
+
#
|
88
|
+
adapter.resource_naming_convention = lambda do |value|
|
89
|
+
inflected_value = DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(value)).gsub('/', '_')
|
90
|
+
|
91
|
+
"#{self.redis.namespace}:#{inflected_value}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.constantize_class_name
|
96
|
+
klass = @@class_name.to_s.classify.constantize
|
97
|
+
|
98
|
+
unless klass < Redcrumbs::Crumb
|
99
|
+
raise ArgumentError, 'Redcrumbs crumb_class must inherit from Redcrumbs::Crumb'
|
100
|
+
end
|
101
|
+
|
102
|
+
klass
|
103
|
+
rescue NameError
|
104
|
+
Crumb
|
55
105
|
end
|
56
106
|
end
|
data/lib/redcrumbs/creation.rb
CHANGED
@@ -2,57 +2,70 @@ module Redcrumbs
|
|
2
2
|
module Creation
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
def crumbs
|
6
|
+
Redcrumbs.crumb_class.all(
|
7
|
+
:subject_type => self.class.to_s,
|
8
|
+
:subject_id => self.id
|
9
|
+
)
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
def watched_changes
|
13
|
+
changes.slice(*self.class.redcrumbs_options[:only])
|
14
|
+
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
if store.has_key?(:only)
|
17
|
-
attributes.reject {|k,v| !store[:only].include?(k.to_sym)}
|
18
|
-
elsif store.has_key?(:except)
|
19
|
-
attributes.reject {|k,v| store[:except].include?(k.to_sym)}
|
20
|
-
else
|
21
|
-
{}
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def attributes_from_storeable_methods
|
26
|
-
store = self.class.redcrumbs_options[:store]
|
27
|
-
if store.has_key?(:methods)
|
28
|
-
# get the methods that actually exist on the model
|
29
|
-
methods = methods_from_array(store[:methods])
|
30
|
-
# inject them into a hash with their outcomes as values
|
31
|
-
methods.inject({}) {|h,a| h.merge(a => send(a))}
|
32
|
-
else
|
33
|
-
{}
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def storeable_attributes_and_method_attributes
|
38
|
-
storeable_attributes.merge(attributes_from_storeable_methods)
|
39
|
-
end
|
16
|
+
def storable_attributes_keys
|
17
|
+
store = self.class.redcrumbs_options[:store]
|
40
18
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
19
|
+
store[:only] or
|
20
|
+
symbolized_attribute_keys(store[:except]) or
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
|
24
|
+
def storeable_attributes
|
25
|
+
attributes.slice *storable_attributes_keys.map(&:to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def storable_methods_names
|
29
|
+
store = self.class.redcrumbs_options[:store]
|
30
|
+
|
31
|
+
if store[:methods]
|
32
|
+
methods.select {|method| store[:methods].include?(method.to_sym)}
|
33
|
+
else
|
34
|
+
[]
|
55
35
|
end
|
56
36
|
end
|
37
|
+
|
38
|
+
# Todo: Fix inconsistent naming; storable vs storeable
|
39
|
+
def storable_methods
|
40
|
+
storable_methods_names.inject({}) {|h, n| h.merge(n.to_s => send(n))}
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialized_as_redcrumbs_subject
|
44
|
+
storeable_attributes.merge(storable_methods)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_crumb
|
48
|
+
n = Redcrumbs.crumb_class.build_with_modifications(self)
|
49
|
+
n.save
|
50
|
+
n
|
51
|
+
end
|
52
|
+
|
53
|
+
# This is called after the record is saved to store the changes on the model, including anything done in before_save validations
|
54
|
+
def notify_changes
|
55
|
+
create_crumb unless watched_changes.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def symbolized_attribute_keys(except = [])
|
61
|
+
return nil unless except
|
62
|
+
|
63
|
+
symbolized_attribute_keys = attributes.dup.symbolize_keys!.keys
|
64
|
+
symbolized_attribute_keys.reject {|key| except.include?(key)}
|
65
|
+
end
|
66
|
+
|
67
|
+
def methods_from_array(array)
|
68
|
+
self.class.instance_methods.select {|method| array.include?(method.to_sym)}
|
69
|
+
end
|
57
70
|
end
|
58
71
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'dm-types'
|
3
|
+
require 'dm-timestamps'
|
4
|
+
require 'dm-redis-adapter'
|
5
|
+
require 'redcrumbs/serializable_association'
|
6
|
+
|
7
|
+
module Redcrumbs
|
8
|
+
class Crumb
|
9
|
+
|
10
|
+
include DataMapper::Resource
|
11
|
+
include Redcrumbs::SerializableAssociation
|
12
|
+
|
13
|
+
property :id, Serial
|
14
|
+
property :modifications, Json, :default => "{}", :lazy => false
|
15
|
+
property :created_at, DateTime
|
16
|
+
property :updated_at, DateTime
|
17
|
+
|
18
|
+
DataMapper.finalize
|
19
|
+
|
20
|
+
after :save, :set_mortality
|
21
|
+
|
22
|
+
serializable_association :creator
|
23
|
+
serializable_association :target
|
24
|
+
serializable_association :subject
|
25
|
+
|
26
|
+
def initialize(params = {})
|
27
|
+
self.subject = params[:subject]
|
28
|
+
self.modifications = params[:modifications]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build_with_modifications(subject)
|
32
|
+
return if subject.watched_changes.empty?
|
33
|
+
|
34
|
+
new(:modifications => subject.watched_changes, :subject => subject)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.created_by(creator)
|
38
|
+
all(:creator_id => creator[Redcrumbs.creator_primary_key]) &
|
39
|
+
all(:creator_type => creator.class.name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.targetted_by(target)
|
43
|
+
all(:target_id => target[Redcrumbs.target_primary_key]) &
|
44
|
+
all(:target_type => target.class.name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Overrides the subject setter created by the SerializableAttributes
|
48
|
+
# module.
|
49
|
+
#
|
50
|
+
def subject=(subject)
|
51
|
+
@subject = subject
|
52
|
+
|
53
|
+
self.stored_subject = subject ? serialize(:subject, subject) : {}
|
54
|
+
self.subject_id = subject ? subject.id : nil
|
55
|
+
assign_type_for(:subject, subject)
|
56
|
+
|
57
|
+
self.target = subject.target if subject.respond_to?(:target)
|
58
|
+
self.creator = subject.creator if subject.respond_to?(:creator)
|
59
|
+
|
60
|
+
subject
|
61
|
+
end
|
62
|
+
|
63
|
+
def redis_key
|
64
|
+
"redcrumbs_crumbs:#{id}" if id
|
65
|
+
end
|
66
|
+
|
67
|
+
# Designed to mimic ActiveRecord's count. Probably not performant and only should be used for tests really
|
68
|
+
def self.count
|
69
|
+
Redcrumbs.redis.keys("redcrumbs_crumbs:*").size - 8
|
70
|
+
end
|
71
|
+
|
72
|
+
# Expiry
|
73
|
+
|
74
|
+
def mortal?
|
75
|
+
return false if new?
|
76
|
+
|
77
|
+
time_to_live >= 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def time_to_live
|
81
|
+
return nil if new?
|
82
|
+
|
83
|
+
@ttl ||= Redcrumbs.redis.ttl(redis_key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def expires_at
|
87
|
+
Time.now + time_to_live if time_to_live
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def set_mortality
|
93
|
+
Redcrumbs.redis.expireat(redis_key, expire_from_now.to_i) if Redcrumbs.mortality
|
94
|
+
end
|
95
|
+
|
96
|
+
def expire_from_now
|
97
|
+
Time.now + Redcrumbs.mortality
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/redcrumbs/options.rb
CHANGED
@@ -6,6 +6,8 @@ module Redcrumbs
|
|
6
6
|
# prepare_redcrumbed_options prepares class level options that customise the behaviour of
|
7
7
|
# redcrumbed. See documentation for a full explanation of redcrumbed options.
|
8
8
|
def prepare_redcrumbed_options(options)
|
9
|
+
options.symbolize_keys!
|
10
|
+
|
9
11
|
defaults = {
|
10
12
|
:only => [],
|
11
13
|
:store => {}
|
@@ -14,13 +16,16 @@ module Redcrumbs
|
|
14
16
|
options.reverse_merge!(defaults)
|
15
17
|
|
16
18
|
options[:only] = Array(options[:only])
|
17
|
-
options[:store] = options[:store]
|
18
19
|
|
19
20
|
class_attribute :redcrumbs_options
|
20
|
-
class_attribute :redcrumbs_callback_options
|
21
21
|
|
22
22
|
self.redcrumbs_options = options.dup
|
23
|
-
|
23
|
+
|
24
|
+
options
|
25
|
+
end
|
26
|
+
|
27
|
+
def redcrumbs_callback_options
|
28
|
+
redcrumbs_options.slice(:if, :unless)
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|