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,193 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'dm-types'
|
3
|
+
|
4
|
+
module Redcrumbs
|
5
|
+
module SerializableAssociation
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
|
9
|
+
base.class_eval do
|
10
|
+
include DataMapper::Resource unless self < DataMapper::Resource
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def serializable_association(name)
|
16
|
+
raise ArgumentError unless name and [:creator, :target, :subject].include?(name)
|
17
|
+
|
18
|
+
property "stored_#{name}".to_sym, DataMapper::Property::Json, :lazy => false
|
19
|
+
property "#{name}_id".to_sym, DataMapper::Property::Integer, :index => true, :lazy => false
|
20
|
+
property "#{name}_type".to_sym, DataMapper::Property::String, :index => true, :lazy => false
|
21
|
+
|
22
|
+
define_setter_for(name)
|
23
|
+
define_getter_for(name)
|
24
|
+
define_loader_for(name)
|
25
|
+
define_load_state_getter(name)
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Define a setter, e.g. object.creator=
|
33
|
+
#
|
34
|
+
def define_setter_for(name)
|
35
|
+
define_method("#{name}=") do |associated|
|
36
|
+
instance_variable_set("@#{name}".to_sym, associated)
|
37
|
+
|
38
|
+
assign_id_for(name, associated)
|
39
|
+
assign_type_for(name, associated)
|
40
|
+
assign_serialized_attributes(name, associated)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Define a getter, e.g. object.creator
|
46
|
+
#
|
47
|
+
def define_getter_for(name)
|
48
|
+
define_method("#{name}") do
|
49
|
+
instance_variable_get("@#{name}") or
|
50
|
+
instance_variable_set("@#{name}", deserialize(name)) or
|
51
|
+
instance_variable_set("@#{name}", load_associated(name))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Define method to force a load of the association from
|
57
|
+
# the database or return it if already loaded.
|
58
|
+
#
|
59
|
+
def define_loader_for(name)
|
60
|
+
define_method("full_#{name}") do
|
61
|
+
if send("has_loaded_#{name}?")
|
62
|
+
instance_variable_get("@#{name}")
|
63
|
+
else
|
64
|
+
instance_variable_set("@#{name}_load_state", true)
|
65
|
+
instance_variable_set("@#{name}", load_associated(name))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Define method to check if association has been fully loaded
|
72
|
+
# from the database.
|
73
|
+
#
|
74
|
+
def define_load_state_getter(name)
|
75
|
+
instance_variable_set("@#{name}_load_state", false)
|
76
|
+
|
77
|
+
define_method("has_loaded_#{name}?") do
|
78
|
+
instance_variable_get("@#{name}_load_state")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
# Load the association from the database.
|
85
|
+
#
|
86
|
+
def load_associated(name)
|
87
|
+
return nil unless association_id = send("#{name}_id")
|
88
|
+
|
89
|
+
class_name = send("#{name}_type") || config_class_name_for(name)
|
90
|
+
klass = class_name.classify.constantize
|
91
|
+
|
92
|
+
primary_key = config_primary_key_for(name) || klass.primary_key
|
93
|
+
|
94
|
+
klass.where(primary_key => association_id).first
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Assign the association id based on default primary key
|
100
|
+
#
|
101
|
+
def assign_id_for(name, associated)
|
102
|
+
id = if associated
|
103
|
+
primary_key = config_primary_key_for(name) or associated.class.primary_key
|
104
|
+
associated[primary_key]
|
105
|
+
end
|
106
|
+
|
107
|
+
send("#{name}_id=", id)
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Assign the association type based on default primary key
|
112
|
+
#
|
113
|
+
def assign_type_for(name, associated)
|
114
|
+
type = associated ? associated.class.name : nil
|
115
|
+
|
116
|
+
send("#{name}_type=", type)
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# Serialize and assign the association
|
121
|
+
#
|
122
|
+
def assign_serialized_attributes(name, associated)
|
123
|
+
serialized = associated ? serialize(name, associated) : {}
|
124
|
+
|
125
|
+
send("stored_#{name}=", serialized)
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# Get the class name from the config options, e.g.
|
130
|
+
# Redcrumbs.creator_class_sym
|
131
|
+
#
|
132
|
+
def config_class_name_for(name)
|
133
|
+
Redcrumbs.send("#{name}_class_sym").to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Get the expected primary key for the association from
|
138
|
+
# the config options.
|
139
|
+
#
|
140
|
+
def config_primary_key_for(name)
|
141
|
+
Redcrumbs.send("#{name}_primary_key")
|
142
|
+
rescue NoMethodError
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
# Serializes a given object by looking for its configuration options
|
147
|
+
# or calling serialization method.
|
148
|
+
#
|
149
|
+
def serialize(name, associated)
|
150
|
+
if name == :subject
|
151
|
+
associated.serialized_as_redcrumbs_subject
|
152
|
+
else
|
153
|
+
keys = Redcrumbs.send("store_#{name}_attributes").dup
|
154
|
+
|
155
|
+
associated.attributes.select {|k,v| keys.include?(k.to_sym)}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Returns a new instance of the associated object based on the
|
161
|
+
# serialized attributes only.
|
162
|
+
#
|
163
|
+
def deserialize(name)
|
164
|
+
properties = send("stored_#{name}")
|
165
|
+
associated_id = send("#{name}_id")
|
166
|
+
|
167
|
+
return nil unless properties.present? and associated_id
|
168
|
+
|
169
|
+
class_name = send("#{name}_type")
|
170
|
+
class_name ||= config_class_name_for(name) unless name == :subject
|
171
|
+
|
172
|
+
instantiate_with_id(class_name, properties, associated_id)
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Return a properties hash that corresponds to the given class's
|
177
|
+
# column names.
|
178
|
+
#
|
179
|
+
def clean_properties(klass, properties)
|
180
|
+
properties.select {|k,v| klass.column_names.include?(k.to_s)}
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
def instantiate_with_id(class_name, properties, associated_id)
|
185
|
+
klass = class_name.classify.constantize
|
186
|
+
properties = clean_properties(klass, properties)
|
187
|
+
|
188
|
+
associated = klass.new(properties, :without_protection => true)
|
189
|
+
associated.id = associated_id
|
190
|
+
associated
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/lib/redcrumbs/users.rb
CHANGED
@@ -1,45 +1,35 @@
|
|
1
1
|
module Redcrumbs
|
2
2
|
# Provides methods for giving user context to crumbs. Retrieves crumbs created by a user (creator) or
|
3
3
|
# affecting a user (target)
|
4
|
-
|
4
|
+
module Users
|
5
5
|
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
module InstanceMethods
|
8
|
-
# Retrieves crumbs related to the user
|
9
|
-
def crumbs_for
|
10
|
-
crumb_or_custom_class.all(:target_id => self[Redcrumbs.target_primary_key], :order => [:created_at.desc])
|
11
|
-
end
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
else
|
40
|
-
Crumb
|
41
|
-
end
|
42
|
-
end
|
7
|
+
# Retrieves crumbs related to the user
|
8
|
+
#
|
9
|
+
def crumbs_for(opts = {})
|
10
|
+
klass = Redcrumbs.crumb_class
|
11
|
+
|
12
|
+
klass.targetted_by(self).all(opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Retrieves crumbs created by the user
|
16
|
+
#
|
17
|
+
def crumbs_by(opts = {})
|
18
|
+
klass = Redcrumbs.crumb_class
|
19
|
+
|
20
|
+
klass.created_by(self).all(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Or queries don't seem to be working with dm-redis-adapter. This
|
24
|
+
# is a temporary workaround.
|
25
|
+
#
|
26
|
+
def crumbs_as_user(opts = {})
|
27
|
+
opts[:limit] ||= 100
|
28
|
+
|
29
|
+
arr = crumbs_by.to_a + crumbs_for.to_a
|
30
|
+
arr.uniq!
|
31
|
+
|
32
|
+
arr.sort_by! {|c| [c.created_at, c.id]}.reverse
|
43
33
|
end
|
44
34
|
end
|
45
35
|
end
|
data/lib/redcrumbs/version.rb
CHANGED
data/redcrumbs.gemspec
CHANGED
@@ -6,20 +6,22 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "redcrumbs"
|
7
7
|
s.version = Redcrumbs::VERSION
|
8
8
|
s.authors = ["John Hope"]
|
9
|
-
s.email = ["
|
10
|
-
s.homepage = "https://github.com/
|
9
|
+
s.email = ["john@shiftdock.com"]
|
10
|
+
s.homepage = "https://github.com/JonMidhir/Redcrumbs"
|
11
11
|
s.summary = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
|
12
12
|
s.description = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
|
13
|
+
s.license = 'MIT'
|
13
14
|
|
14
15
|
s.rubyforge_project = "redcrumbs"
|
16
|
+
|
17
|
+
s.add_dependency 'data_mapper', '>= 1.2.0'
|
18
|
+
s.add_dependency 'redis', '>= 2.2.2'
|
19
|
+
s.add_dependency 'dm-redis-adapter', '>= 0.6.2'
|
20
|
+
s.add_dependency 'redis-namespace', '>= 1.3.0'
|
21
|
+
s.add_dependency 'activerecord', '>= 3.1', '< 5'
|
22
|
+
s.add_dependency 'activesupport', '>= 3.1', '< 5'
|
15
23
|
|
16
24
|
s.files = `git ls-files`.split("\n")
|
17
|
-
s.test_files =
|
18
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
+
s.test_files = s.files.grep(/^spec/)
|
19
26
|
s.require_paths = ["lib"]
|
20
|
-
|
21
|
-
s.add_dependency 'data_mapper', '>= 1.2.0'
|
22
|
-
s.add_dependency 'dm-redis-adapter', '~> 0.6.2'
|
23
|
-
s.add_dependency 'redis', '~> 2.2.2'
|
24
|
-
s.add_dependency 'redis-namespace', '>= 1.1.0'
|
25
27
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redcrumbs do
|
4
|
+
describe '.creator_class_sym' do
|
5
|
+
subject { Redcrumbs.creator_class_sym }
|
6
|
+
|
7
|
+
context 'when unchanged' do
|
8
|
+
it { is_expected.to eq(:user) }
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when changed to :game' do
|
12
|
+
before { Redcrumbs.creator_class_sym = :game }
|
13
|
+
after { Redcrumbs.creator_class_sym = :user }
|
14
|
+
|
15
|
+
it { is_expected.to eq(:game)}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
describe '.creator_primary_key' do
|
21
|
+
subject { Redcrumbs.creator_primary_key }
|
22
|
+
|
23
|
+
context 'when unchanged' do
|
24
|
+
it { is_expected.to eq('id') }
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when changed to :name' do
|
28
|
+
before { Redcrumbs.creator_primary_key = :name }
|
29
|
+
after { Redcrumbs.creator_primary_key = :id }
|
30
|
+
|
31
|
+
it { is_expected.to eq(:name)}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
describe '.target_class_sym' do
|
37
|
+
subject { Redcrumbs.target_class_sym }
|
38
|
+
|
39
|
+
context 'when unchanged' do
|
40
|
+
it { is_expected.to eq(:user) }
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when changed to :game' do
|
44
|
+
before { Redcrumbs.target_class_sym = :game }
|
45
|
+
after { Redcrumbs.target_class_sym = :user }
|
46
|
+
|
47
|
+
it { is_expected.to eq(:game)}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
describe '.target_primary_key' do
|
53
|
+
subject { Redcrumbs.target_primary_key }
|
54
|
+
|
55
|
+
context 'when unchanged' do
|
56
|
+
it { is_expected.to eq('id') }
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when changed to :name' do
|
60
|
+
before { Redcrumbs.target_primary_key = :name }
|
61
|
+
after { Redcrumbs.target_primary_key = :id }
|
62
|
+
|
63
|
+
it { is_expected.to eq(:name)}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
describe '.store_creator_attributes' do
|
69
|
+
subject { Redcrumbs.store_creator_attributes }
|
70
|
+
|
71
|
+
context 'when unchanged' do
|
72
|
+
it { is_expected.to eq([]) }
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when given attribute keys' do
|
76
|
+
before { Redcrumbs.store_creator_attributes = [:id, :name] }
|
77
|
+
after { Redcrumbs.store_creator_attributes = [] }
|
78
|
+
|
79
|
+
it { is_expected.to eq([:id, :name])}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
describe '.store_target_attributes' do
|
85
|
+
subject { Redcrumbs.store_target_attributes }
|
86
|
+
|
87
|
+
context 'when unchanged' do
|
88
|
+
it { is_expected.to eq([]) }
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when given attribute keys' do
|
92
|
+
before { Redcrumbs.store_target_attributes = [:id, :name] }
|
93
|
+
after { Redcrumbs.store_target_attributes = [] }
|
94
|
+
|
95
|
+
it { is_expected.to eq([:id, :name])}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
describe '.redis' do
|
101
|
+
let!(:default_redis) { Redcrumbs.redis }
|
102
|
+
subject { Redcrumbs.redis }
|
103
|
+
|
104
|
+
context 'when given a URL string with port and scheme' do
|
105
|
+
before { Redcrumbs.redis = 'redis://localhost:6379' }
|
106
|
+
after { Redcrumbs.redis = default_redis }
|
107
|
+
|
108
|
+
it { expect(subject.namespace).to eq(:redcrumbs) }
|
109
|
+
it { expect(subject.client.host).to eq('localhost') }
|
110
|
+
it { expect(subject.client.port).to eq(6379) }
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when given a URL string without scheme' do
|
114
|
+
before { Redcrumbs.redis = 'localhost:6379' }
|
115
|
+
after { Redcrumbs.redis = default_redis }
|
116
|
+
|
117
|
+
it { expect(subject.namespace).to eq(:redcrumbs) }
|
118
|
+
it { expect(subject.client.host).to eq('localhost') }
|
119
|
+
it { expect(subject.client.port).to eq(6379) }
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when given a URL string with namespace' do
|
123
|
+
before { Redcrumbs.redis = 'localhost:6379/some_namespace' }
|
124
|
+
after { Redcrumbs.redis = default_redis }
|
125
|
+
|
126
|
+
it { expect(subject.namespace).to eq('some_namespace') }
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when given an existing redis client' do
|
130
|
+
let(:redis) { Redis.new }
|
131
|
+
before { Redcrumbs.redis = redis }
|
132
|
+
after { Redcrumbs.redis = default_redis }
|
133
|
+
|
134
|
+
it { expect(subject.redis).to eq(redis) }
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'when given an existing redis namespace' do
|
138
|
+
let(:redis) { Redis::Namespace.new('some_namespace') }
|
139
|
+
before { Redcrumbs.redis = redis }
|
140
|
+
after { Redcrumbs.redis = default_redis }
|
141
|
+
|
142
|
+
it { is_expected.to eq(redis) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
describe '.crumb_class' do
|
148
|
+
subject { Redcrumbs.crumb_class }
|
149
|
+
|
150
|
+
context 'when class_name unchanged' do
|
151
|
+
it { is_expected.to be(Redcrumbs::Crumb) }
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'when class_name set to unknown class' do
|
155
|
+
before { Redcrumbs.class_name = :foo }
|
156
|
+
after { Redcrumbs.class_name = nil }
|
157
|
+
|
158
|
+
it { is_expected.to be(Redcrumbs::Crumb) }
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when class doesnt inherit from Crumb' do
|
162
|
+
before { Redcrumbs.class_name = :game }
|
163
|
+
after { Redcrumbs.class_name = nil }
|
164
|
+
|
165
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|