activr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module Activr
2
+
3
+ #
4
+ # Rails Context holder
5
+ #
6
+ class RailsCtx
7
+
8
+ class_attribute :controller
9
+ self.controller = nil
10
+
11
+ class << self
12
+
13
+ # Get current Rails View context
14
+ #
15
+ # @return [ActionView::Base] Rails view instance
16
+ def view_context
17
+ @view_context ||= if defined?(::Rails)
18
+ rails_controller = self.controller || begin
19
+ fake_controller = ApplicationController.new
20
+ fake_controller.request = ActionController::TestRequest.new if defined?(ActionController::TestRequest)
21
+ fake_controller
22
+ end
23
+
24
+ rails_controller.view_context
25
+ end
26
+ end
27
+
28
+ # Clear memoization of current Rails view context
29
+ def clear_view_context!
30
+ @view_context = nil
31
+ end
32
+
33
+ end # class << self
34
+
35
+ end # class RailsCtx
36
+
37
+ end # module Activr
@@ -0,0 +1,73 @@
1
+ module Activr
2
+
3
+ # Hook into Rails
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "activr.set_conf", :after => 'mongoid.load-config' do |app|
6
+ Activr.configure do |config|
7
+ # setup app path
8
+ activr_dir = File.join(::Rails.root, 'app', 'activr')
9
+ if !File.exists?(activr_dir)
10
+ activr_dir = File.join(::Rails.root, 'app')
11
+ end
12
+
13
+ config.app_path = activr_dir
14
+
15
+ use_mongoid_conn = Fwissr['/activr/mongodb/uri'].blank? &&
16
+ (Fwissr['/activr/skip_mongoid_railtie'] != true) &&
17
+ (ENV['ACTIVR_SKIP_MONGOID_RAILTIE'] != 'true') &&
18
+ defined?(Mongoid)
19
+
20
+ if use_mongoid_conn
21
+ # get mongoid conf
22
+ if Mongoid::VERSION.start_with?("2.")
23
+ # Mongoid 2
24
+ config.mongodb[:uri] = "mongodb://#{Mongoid.master.connection.host}:#{Mongoid.master.connection.port}/#{Mongoid.master.name}"
25
+ elsif Mongoid.sessions[:default] && !Mongoid.sessions[:default][:uri].blank?
26
+ # Mongoid >= 3 with :uri setting
27
+ config.mongodb[:uri] = Mongoid.sessions[:default][:uri].dup
28
+ elsif Mongoid.sessions[:default] && !Mongoid.sessions[:default][:database].blank? && !Mongoid.sessions[:default][:hosts].blank?
29
+ # Mongoid >= 3 without :uri setting
30
+ config.mongodb[:uri] = "mongodb://#{Mongoid.sessions[:default][:hosts].first}/#{Mongoid.sessions[:default][:database]}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ initializer 'activr.autoload', :after => "activr.set_conf", :before => :set_autoload_paths do |app|
37
+ app.config.autoload_paths += [
38
+ File.join(Activr.config.app_path, 'activities'),
39
+ File.join(Activr.config.app_path, 'timelines'),
40
+ ]
41
+ end
42
+
43
+ initializer "activr.setup_async_hooks" do |app|
44
+ if defined?(::Resque) && (ENV['ACTIVR_FORCE_SYNC'] != 'true')
45
+ Activr.configure do |config|
46
+ config.async[:route_activity] ||= Activr::Async::Resque::RouteActivity
47
+ config.async[:timeline_handle] ||= Activr::Async::Resque::TimelineHandle
48
+ end
49
+ end
50
+ end
51
+
52
+ initializer "activr.setup_action_controller" do |app|
53
+ ActiveSupport.on_load :action_controller do
54
+ self.class_eval do
55
+ before_filter do |controller|
56
+ Activr::RailsCtx.clear_view_context!
57
+ Activr::RailsCtx.controller = controller
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ rake_tasks do
64
+ load "activr/railties/activr.rake"
65
+ end
66
+
67
+ config.after_initialize do |app|
68
+ # setup registry
69
+ Activr.setup
70
+ end
71
+ end # class Railtie
72
+
73
+ end # module Activr
@@ -0,0 +1,14 @@
1
+ namespace :activr do
2
+
3
+ desc "Create the indexes"
4
+ task :create_indexes => :environment do
5
+ ::Rails.application.eager_load!
6
+
7
+ Activr.setup
8
+
9
+ Activr.storage.create_indexes do |index_name|
10
+ puts "Created index: #{index_name}"
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,268 @@
1
+ module Activr
2
+
3
+ #
4
+ # The registry holds all activities, entities, timelines and timeline entries classes defined in the application
5
+ #
6
+ # The registry singleton is accessible with {Activr.registry}
7
+ #
8
+ class Registry
9
+
10
+ # @return [Hash{Symbol=>Class}] model class associated to entity name
11
+ attr_reader :entity_classes
12
+
13
+ # @return [Hash{Class=>Array<Symbol>}] entity names for activity class
14
+ attr_reader :activity_entities
15
+
16
+ # Init
17
+ def initialize
18
+ @setup = false
19
+
20
+ @timelines = nil
21
+ @timeline_entries = nil
22
+ @activities = nil
23
+ @entities = nil
24
+ @models = nil
25
+
26
+ @entity_classes = { }
27
+ @activity_entities = { }
28
+
29
+ @timeline_entities_for_model = { }
30
+ @activity_entities_for_model = { }
31
+ end
32
+
33
+ # Setup registry
34
+ def setup
35
+ return if @setup
36
+
37
+ # eagger load all classes
38
+ self.activities
39
+ self.timelines
40
+ self.timeline_entries
41
+
42
+ @setup = true
43
+ end
44
+
45
+
46
+ #
47
+ # Classes
48
+ #
49
+
50
+ # Get all registered timelines
51
+ #
52
+ # @return [Hash{String=>Class}] A hash of `<timeline kind> => <timeline class>`
53
+ def timelines
54
+ @timelines ||= self.classes_from_path(Activr.timelines_path)
55
+ end
56
+
57
+ # Get class for given timeline kind
58
+ #
59
+ # @param timeline_kind [String] Timeline kind
60
+ # @return [Class] Timeline class
61
+ def class_for_timeline(timeline_kind)
62
+ result = self.timelines[timeline_kind]
63
+ raise "No class defined for timeline kind: #{timeline_kind}" if result.blank?
64
+ result
65
+ end
66
+
67
+ # Get all registered timeline entries
68
+ #
69
+ # @return [Hash{String=>Hash{String=>Class}}] A hash of `<timeline kind> => { <route kind> => <timeline entry class>, ... }`
70
+ def timeline_entries
71
+ @timeline_entries ||= begin
72
+ result = { }
73
+
74
+ self.timelines.each do |(timeline_kind, timeline_class)|
75
+ dir_name = Activr::Utils.kind_for_class(timeline_class)
76
+ dir_path = File.join(Activr.timelines_path, dir_name)
77
+
78
+ if !File.directory?(dir_path)
79
+ dir_name = Activr::Utils.kind_for_class(timeline_class, 'timeline')
80
+ dir_path = File.join(Activr.timelines_path, dir_name)
81
+ end
82
+
83
+ if File.directory?(dir_path)
84
+ result[timeline_kind] = { }
85
+
86
+ Dir["#{dir_path}/*.rb"].sort.inject(result[timeline_kind]) do |memo, file_path|
87
+ base_name = File.basename(file_path, '.rb')
88
+
89
+ # skip base class
90
+ if (base_name != "base_timeline_entry")
91
+ klass = "#{timeline_class.name}::#{base_name.camelize}".constantize
92
+
93
+ route_kind = if (match_data = base_name.match(/(.+)_timeline_entry$/))
94
+ match_data[1]
95
+ else
96
+ base_name
97
+ end
98
+
99
+ route = timeline_class.routes.find do |timeline_route|
100
+ timeline_route.kind == route_kind
101
+ end
102
+
103
+ raise "Timeline entry class found for an unspecified timeline route: #{file_path} / routes: #{timeline_class.routes.inspect}" unless route
104
+ memo[route_kind] = klass
105
+ end
106
+
107
+ memo
108
+ end
109
+ end
110
+ end
111
+
112
+ result
113
+ end
114
+ end
115
+
116
+ # Get class for timeline entry corresponding to given route in given timeline
117
+ #
118
+ # @param timeline_kind [String] Timeline kind
119
+ # @param route_kind [String] Route kind
120
+ # @return [Class] Timeline entry class
121
+ def class_for_timeline_entry(timeline_kind, route_kind)
122
+ (self.timeline_entries[timeline_kind] && self.timeline_entries[timeline_kind][route_kind]) || Activr::Timeline::Entry
123
+ end
124
+
125
+ # Get all registered activities
126
+ #
127
+ # @return [Hash{String=>Class}] A hash of `<activity kind> => <activity class>`
128
+ def activities
129
+ @activities ||= self.classes_from_path(Activr.activities_path)
130
+ end
131
+
132
+ # Get class for given activity
133
+ #
134
+ # @param activity_kind [String] Activity kind
135
+ # @return [Class] Activity class
136
+ def class_for_activity(activity_kind)
137
+ result = self.activities[activity_kind]
138
+ raise "No class defined for activity kind: #{activity_kind}" if result.blank?
139
+ result
140
+ end
141
+
142
+ # Get all registered entities
143
+ #
144
+ # @return [Hash{Symbol=>Array<Class>}] A hash of `:<entity name> => [ <activity class>, <activity class>, ... ]`
145
+ def entities
146
+ # loading activities triggers calls to #add_entity method
147
+ self.activities if @entities.blank?
148
+
149
+ @entities || { }
150
+ end
151
+
152
+ # Get all registered entities names
153
+ #
154
+ # @return [Array<Symbol>] List of entities names
155
+ def entities_names
156
+ @entities_names ||= self.entities.keys
157
+ end
158
+
159
+ # Register an entity
160
+ #
161
+ # @param entity_name [Symbol] Entity name
162
+ # @param entity_options [Hash] Entity options
163
+ # @param activity_klass [Class] Activity class that uses that entity
164
+ def add_entity(entity_name, entity_options, activity_klass)
165
+ entity_name = entity_name.to_sym
166
+
167
+ if @entity_classes[entity_name] && (@entity_classes[entity_name].name != entity_options[:class].name)
168
+ # otherwise this would break timeline entries deletion mecanism
169
+ raise "Entity name #{entity_name} already used with class #{@entity_classes[entity_name]}, can't redefine it with class #{entity_options[:class]}"
170
+ end
171
+
172
+ # class for entity
173
+ @entity_classes[entity_name] = entity_options[:class]
174
+
175
+ # entities for activity
176
+ @activity_entities[activity_klass] ||= [ ]
177
+ @activity_entities[activity_klass] << entity_name
178
+
179
+ # entities
180
+ @entities ||= { }
181
+ @entities[entity_name] ||= { }
182
+
183
+ if !@entities[entity_name][activity_klass].blank?
184
+ raise "Entity name #{entity_name} already used for activity: #{activity_klass}"
185
+ end
186
+
187
+ @entities[entity_name][activity_klass] = entity_options
188
+ end
189
+
190
+ # Get all models that included mixin {Activr::Entity::ModelMixin}
191
+ #
192
+ # @return [Array<Class>] List of model classes
193
+ def models
194
+ # loading activities triggers models loading
195
+ self.activities if @models.blank?
196
+
197
+ @models || [ ]
198
+ end
199
+
200
+ # Register a model
201
+ #
202
+ # @param model_class [Class] Model class
203
+ def add_model(model_class)
204
+ @models ||= [ ]
205
+ @models << model_class
206
+ end
207
+
208
+ # Get all entities names for given model class
209
+ def activity_entities_for_model(model_class)
210
+ @activity_entities_for_model[model_class] ||= begin
211
+ result = [ ]
212
+
213
+ @entity_classes.each do |entity_name, entity_class|
214
+ result << entity_name if (entity_class == model_class)
215
+ end
216
+
217
+ result
218
+ end
219
+ end
220
+
221
+ # Get all entities names by timelines that can have a reference to given model class
222
+ #
223
+ # @param model_class [Class] Model class
224
+ # @return [Hash{Class=>Array<Symbol>}] Lists of entities names indexed by timeline class
225
+ def timeline_entities_for_model(model_class)
226
+ @timeline_entities_for_model[model_class] ||= begin
227
+ result = { }
228
+
229
+ self.timelines.each do |timeline_kind, timeline_class|
230
+ result[timeline_class] = [ ]
231
+
232
+ timeline_class.routes.each do |route|
233
+ entities_ary = @activity_entities[route.activity_class]
234
+ (entities_ary || [ ]).each do |entity_name|
235
+ result[timeline_class] << entity_name if (@entity_classes[entity_name] == model_class)
236
+ end
237
+ end
238
+
239
+ result[timeline_class].uniq!
240
+ end
241
+
242
+ result
243
+ end
244
+ end
245
+
246
+ # Find all classes in given directory
247
+ #
248
+ # @api private
249
+ #
250
+ # @param dir_path [String] Directory path
251
+ # @return [Hash{String=>Class}] Hash of `<kind> => <Class>`
252
+ def classes_from_path(dir_path)
253
+ Dir["#{dir_path}/*.rb"].sort.inject({ }) do |memo, file_path|
254
+ klass = File.basename(file_path, '.rb').camelize.constantize
255
+
256
+ if !memo[klass.kind].nil?
257
+ raise "Kind #{klass.kind} already used by class #{memo[klass.kind]} so can't use it for class #{klass}"
258
+ end
259
+
260
+ memo[klass.kind] = klass
261
+
262
+ memo
263
+ end
264
+ end
265
+
266
+ end # class Registry
267
+
268
+ end # module Activr
@@ -0,0 +1,404 @@
1
+ module Activr
2
+
3
+ #
4
+ # The storage is the component that uses the database driver to serialize/unserialize activities and timeline entries.
5
+ #
6
+ # The storage singleton is accessible with {Activr.storage}
7
+ #
8
+ class Storage
9
+
10
+ autoload :MongoDriver, 'activr/storage/mongo_driver'
11
+
12
+ # @return [MongoDriver] database driver
13
+ attr_reader :driver
14
+
15
+ def initialize
16
+ @driver = Activr::Storage::MongoDriver.new
17
+
18
+ @hooks = { }
19
+ end
20
+
21
+ # Is it a valid document id
22
+ #
23
+ # @param doc_id [Object] Document id to check
24
+ # @return [true, false]
25
+ def valid_id?(doc_id)
26
+ self.driver.valid_id?(doc_id)
27
+ end
28
+
29
+ # Is it a serialized document id
30
+ #
31
+ # @return [true,false]
32
+ def serialized_id?(doc_id)
33
+ self.driver.serialized_id?(doc_id)
34
+ end
35
+
36
+ # Unserialize a document id
37
+ #
38
+ # @param doc_id [Object] Document id
39
+ # @return [Object] Unserialized document id
40
+ def unserialize_id(doc_id)
41
+ self.driver.unserialize_id(doc_id)
42
+ end
43
+
44
+ # Unserialize given parameter only if it is a serialized document id
45
+ #
46
+ # @param doc_id [Object] Document id
47
+ # @return [Object] Unserialized or unmodified document id
48
+ def unserialize_id_if_necessary(doc_id)
49
+ self.serialized_id?(doc_id) ? self.unserialize_id(doc_id) : doc_id
50
+ end
51
+
52
+
53
+ #
54
+ # Activities
55
+ #
56
+
57
+ # Insert a new activity
58
+ #
59
+ # @param activity [Activity] Activity to insert
60
+ # @return [Object] The inserted activity id
61
+ def insert_activity(activity)
62
+ # serialize
63
+ activity_hash = activity.to_hash
64
+
65
+ # run hook
66
+ self.run_hook(:will_insert_activity, activity_hash)
67
+
68
+ # insert
69
+ self.driver.insert_activity(activity_hash)
70
+ end
71
+
72
+ # Find an activity
73
+ #
74
+ # @param activity_id [Object] Activity id to find
75
+ # @return [Activity, Nil] An activity instance or `nil` if not found
76
+ def find_activity(activity_id)
77
+ activity_hash = self.driver.find_activity(activity_id)
78
+ if activity_hash
79
+ # run hook
80
+ self.run_hook(:did_find_activity, activity_hash)
81
+
82
+ # unserialize
83
+ Activr::Activity.from_hash(activity_hash)
84
+ else
85
+ nil
86
+ end
87
+ end
88
+
89
+ # Find latest activities
90
+ #
91
+ # @note If you use others selectors then 'limit' argument and 'skip' option then you have to setup corresponding indexes in database.
92
+ #
93
+ # @param limit [Integer] Max number of activities to find
94
+ # @param options [Hash] Options hash
95
+ # @option options [Integer] :skip Number of activities to skip (default: 0)
96
+ # @option options [Time] :before Find activities generated before that datetime (excluding)
97
+ # @option options [Time] :after Find activities generated after that datetime (excluding)
98
+ # @option options [Hash{Sym=>String}] :entities Filter by entities values (empty means 'all values')
99
+ # @option options [Array<Class>] :only Find only these activities
100
+ # @option options [Array<Class>] :except Skip these activities
101
+ # @return [Array<Activity>] An array of activities
102
+ def find_activities(limit, options = { })
103
+ # default options
104
+ options = {
105
+ :skip => 0,
106
+ :before => nil,
107
+ :after => nil,
108
+ :entities => { },
109
+ :only => [ ],
110
+ :except => [ ],
111
+ }.merge(options)
112
+
113
+ options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
114
+
115
+ # find
116
+ result = self.driver.find_activities(limit, options).map do |activity_hash|
117
+ # run hook
118
+ self.run_hook(:did_find_activity, activity_hash)
119
+
120
+ # unserialize
121
+ Activr::Activity.from_hash(activity_hash)
122
+ end
123
+
124
+ result
125
+ end
126
+
127
+ # Count number of activities
128
+ #
129
+ # @note If you use one of options selectors then you have to setup corresponding indexes in database.
130
+ #
131
+ # @param options [Hash] Options hash
132
+ # @option options [Time] :before Find activities generated before that datetime (excluding)
133
+ # @option options [Time] :after Find activities generated after that datetime (excluding)
134
+ # @option options [Hash{Sym=>String}] :entities Filter by entities values (empty means 'all values')
135
+ # @option options [Array<Class>] :only Find only these activities
136
+ # @option options [Array<Class>] :except Skip these activities
137
+ # @return [Integer] Number of activities
138
+ def count_activities(options = { })
139
+ # default options
140
+ options = {
141
+ :before => nil,
142
+ :after => nil,
143
+ :entities => { },
144
+ :only => [ ],
145
+ :except => [ ],
146
+ }.merge(options)
147
+
148
+ options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
149
+
150
+ # count
151
+ self.driver.count_activities(options)
152
+ end
153
+
154
+ # Find number of duplicate activities
155
+ #
156
+ # @param activity [Activity] Activity to search
157
+ # @param after [Time] Search after that datetime
158
+ # @return [Integer] Number of activity duplicates
159
+ def count_duplicate_activities(activity, after)
160
+ entities = { }
161
+
162
+ activity.entities.each do |entity_name, entity|
163
+ entities[entity_name.to_sym] = entity.model_id
164
+ end
165
+
166
+ self.count_activities({
167
+ :only => activity.class,
168
+ :entities => entities,
169
+ :after => after,
170
+ })
171
+ end
172
+
173
+ # Delete activities referring to given entity model instance
174
+ #
175
+ # @param model [Object] Model instance
176
+ def delete_activities_for_entity_model(model)
177
+ Activr.registry.activity_entities_for_model(model.class).each do |entity_name|
178
+ self.driver.delete_activities(:entities => { entity_name => model.id })
179
+ end
180
+ end
181
+
182
+
183
+ #
184
+ # Timeline Entries
185
+ #
186
+
187
+ # Insert a new timeline entry
188
+ #
189
+ # @param timeline_entry [Timeline::Entry] Timeline entry to insert
190
+ # @return [Object] Inserted timeline entry id
191
+ def insert_timeline_entry(timeline_entry)
192
+ # serialize
193
+ timeline_entry_hash = timeline_entry.to_hash
194
+
195
+ # run hook
196
+ self.run_hook(:will_insert_timeline_entry, timeline_entry_hash, timeline_entry.timeline.class)
197
+
198
+ # insert
199
+ self.driver.insert_timeline_entry(timeline_entry.timeline.kind, timeline_entry_hash)
200
+ end
201
+
202
+ # Find a timeline entry
203
+ #
204
+ # @param timeline [Timeline] Timeline instance
205
+ # @param tl_entry_id [Object] Timeline entry id
206
+ # @return [Timeline::Entry, Nil] Found timeline entry
207
+ def find_timeline_entry(timeline, tl_entry_id)
208
+ timeline_entry_hash = self.driver.find_timeline_entry(timeline.kind, tl_entry_id)
209
+ if timeline_entry_hash
210
+ # run hook
211
+ self.run_hook(:did_find_timeline_entry, timeline_entry_hash, timeline.class)
212
+
213
+ # unserialize
214
+ Activr::Timeline::Entry.from_hash(timeline_entry_hash, timeline)
215
+ else
216
+ nil
217
+ end
218
+ end
219
+
220
+ # Find timeline entries by descending timestamp
221
+ #
222
+ # @param timeline [Timeline] Timeline instance
223
+ # @param limit [Integer] Max number of entries to find
224
+ # @param options [Hash] Options hash
225
+ # @option options [Integer] :skip Number of entries to skip (default: 0)
226
+ # @option options [Array<Timeline::Route>] :only An array of routes to fetch
227
+ # @return [Array<Timeline::Entry>] An array of timeline entries
228
+ def find_timeline(timeline, limit, options = { })
229
+ options = {
230
+ :skip => 0,
231
+ :only => [ ],
232
+ }.merge(options)
233
+
234
+ options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
235
+
236
+ result = self.driver.find_timeline_entries(timeline.kind, timeline.recipient_id, limit, options).map do |timeline_entry_hash|
237
+ # run hook
238
+ self.run_hook(:did_find_timeline_entry, timeline_entry_hash, timeline.class)
239
+
240
+ # unserialize
241
+ Activr::Timeline::Entry.from_hash(timeline_entry_hash, timeline)
242
+ end
243
+
244
+ result
245
+ end
246
+
247
+ # Count number of timeline entries
248
+ #
249
+ # @param timeline [Timeline] Timeline instance
250
+ # @param options [Hash] Options hash
251
+ # @option options [Array<Timeline::Route>] :only An array of routes to count
252
+ # @return [Integer] Number of timeline entries in given timeline
253
+ def count_timeline(timeline, options = { })
254
+ options = {
255
+ :only => [ ],
256
+ }.merge(options)
257
+
258
+ options[:only] = [ options[:only] ] if (options[:only] && !options[:only].is_a?(Array))
259
+
260
+ self.driver.count_timeline_entries(timeline.kind, timeline.recipient_id, options)
261
+ end
262
+
263
+ # Delete timeline entries
264
+ #
265
+ # @param timeline [Timeline] Timeline instance
266
+ # @param options [Hash] Options hash
267
+ # @option options [Time] :before Delete only timeline entries which timestamp is before that datetime (excluding)
268
+ # @option options [Hash{Sym=>String}] :entity Delete only timeline entries with these entities values
269
+ def delete_timeline(timeline, options = { })
270
+ # default options
271
+ options = {
272
+ :before => nil,
273
+ :entities => { },
274
+ }.merge(options)
275
+
276
+ self.driver.delete_timeline_entries(timeline.kind, timeline.recipient_id, options)
277
+ end
278
+
279
+ # Delete timeline entries referring to given entity model instance
280
+ #
281
+ # @param model [Object] Model instance
282
+ def delete_timeline_entries_for_entity_model(model)
283
+ Activr.registry.timeline_entities_for_model(model.class).each do |timeline_class, entities|
284
+ entities.each do |entity_name|
285
+ self.driver.delete_timeline_entries(timeline_class.kind, nil, :entities => { entity_name => model.id })
286
+ end
287
+ end
288
+ end
289
+
290
+
291
+ #
292
+ # Indexes
293
+ #
294
+
295
+ # Ensure all necessary indexes
296
+ #
297
+ # @yield [String] Created index name
298
+ def create_indexes
299
+ self.driver.create_indexes
300
+ end
301
+
302
+
303
+ #
304
+ # Hooks
305
+ #
306
+
307
+ # Hook: run just before inserting an activity document in the database
308
+ #
309
+ # @example Insert the 'foo' meta into all activities
310
+ #
311
+ # Activr.storage.will_insert_activity do |activity_hash|
312
+ # activity_hash['meta'] ||= { }
313
+ # activity_hash['meta']['foo'] = 'bar'
314
+ # end
315
+ #
316
+ def will_insert_activity(&block)
317
+ register_hook(:will_insert_activity, block)
318
+ end
319
+
320
+ # Hook: run just after fetching an activity document from the database
321
+ #
322
+ # @example Ignore the 'foo' meta
323
+ #
324
+ # Activr.storage.did_find_activity do |activity_hash|
325
+ # if activity_hash['meta']
326
+ # activity_hash['meta'].delete('foo')
327
+ # end
328
+ # end
329
+ #
330
+ def did_find_activity(&block)
331
+ register_hook(:did_find_activity, block)
332
+ end
333
+
334
+ # Hook: run just before inserting a timeline entry document in the database
335
+ #
336
+ # @example Insert the 'bar' field into all timeline entries documents
337
+ #
338
+ # Activr.storage.will_insert_timeline_entry do |timeline_entry_hash, timeline_class|
339
+ # timeline_entry_hash['bar'] = 'baz'
340
+ # end
341
+ #
342
+ def will_insert_timeline_entry(&block)
343
+ register_hook(:will_insert_timeline_entry, block)
344
+ end
345
+
346
+ # Hook: run just after fetching a timeline entry document from the database
347
+ #
348
+ # @example Ignore the 'bar' field
349
+ #
350
+ # Activr.storage.did_find_timeline_entry do |timeline_entry_hash, timeline_class|
351
+ # timeline_entry_hash.delete('bar')
352
+ # end
353
+ #
354
+ def did_find_timeline_entry(&block)
355
+ register_hook(:did_find_timeline_entry, block)
356
+ end
357
+
358
+
359
+ # Register a hook
360
+ #
361
+ # @api private
362
+ #
363
+ # @param name [Symbol] Hook name
364
+ # @param block [Proc] Hook code
365
+ def register_hook(name, block)
366
+ @hooks[name] ||= [ ]
367
+ @hooks[name] << block
368
+ end
369
+
370
+ # Get hooks
371
+ #
372
+ # @api private
373
+ # @note Returns all hooks if `name` is `nil`
374
+ #
375
+ # @param name [Symbol] Hook name
376
+ # @return [Array<Proc>] List of hooks
377
+ def hooks(name = nil)
378
+ name ? (@hooks[name] || [ ]) : @hooks
379
+ end
380
+
381
+ # Run a hook
382
+ #
383
+ # @api private
384
+ #
385
+ # @param name [Symbol] Hook name
386
+ # @param args [Array] Hook arguments
387
+ def run_hook(name, *args)
388
+ return if @hooks[name].blank?
389
+
390
+ @hooks[name].each do |hook|
391
+ args.any? ? hook.call(*args) : hook.call
392
+ end
393
+ end
394
+
395
+ # Reset all hooks
396
+ #
397
+ # @api private
398
+ def clear_hooks!
399
+ @hooks = { }
400
+ end
401
+
402
+ end # class Storage
403
+
404
+ end # module Activr