wazowski 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b3b3ba72596ae107acd23c4e967a7f67227a86bb4f12484bdd516427915a3445
4
+ data.tar.gz: ad1645836bb7b460f82569bc2c69de577b8af4a24f40f3063ccd0ef8920e2e9e
5
+ SHA512:
6
+ metadata.gz: 88b7a93e0ecab89bdedd9f352f56a392ef0a47828ba4c1c0abaa2707ae2ee218a5b4466004a6b29204d0764c46df4bf2c843462723addf135e74d8832f81799b
7
+ data.tar.gz: 8996a9448947f9b02f416bfbf731766a27b10074e8ca6851796ea62050cc0759826b75d94abf2bea55aa6214f629e3f78ab69988ca882c53540667bc935d40aa
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .env
@@ -0,0 +1,13 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.3.4
6
+ - 2.4.0
7
+ services:
8
+ - postgresql
9
+ before_install: gem install bundler -v 1.16.0
10
+ env:
11
+ - WAZOWSKI_PG_DATABASE=travis_ci_test WAZOWSKI_PG_USERNAME=postgres
12
+ before_script:
13
+ - psql -c 'create database travis_ci_test;' -U postgres
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in wazowski.gemspec
6
+ gemspec
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ wazowski (0.1.0)
5
+ activerecord (>= 4.2)
6
+ activesupport (>= 4.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (5.1.4)
12
+ activesupport (= 5.1.4)
13
+ activerecord (5.1.4)
14
+ activemodel (= 5.1.4)
15
+ activesupport (= 5.1.4)
16
+ arel (~> 8.0)
17
+ activesupport (5.1.4)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (~> 0.7)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+ arel (8.0.0)
23
+ concurrent-ruby (1.0.5)
24
+ dotenv (2.2.1)
25
+ i18n (0.9.1)
26
+ concurrent-ruby (~> 1.0)
27
+ minitest (5.11.1)
28
+ pg (0.21.0)
29
+ rake (10.5.0)
30
+ thread_safe (0.3.6)
31
+ tzinfo (1.2.4)
32
+ thread_safe (~> 0.1)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ bundler (~> 1.16)
39
+ dotenv
40
+ minitest (~> 5.0)
41
+ pg
42
+ rake (~> 10.0)
43
+ wazowski!
44
+
45
+ BUNDLED WITH
46
+ 1.16.0
@@ -0,0 +1,291 @@
1
+ [![Build Status](https://travis-ci.org/rogercampos/wazowski.svg?branch=master)](https://travis-ci.org/rogercampos/wazowski)
2
+
3
+
4
+ # Wazowski
5
+
6
+ You can use this library to observe changes on data and execute your code when those changes occur.
7
+
8
+ Example:
9
+
10
+ ```ruby
11
+ class Something < Wazowski::Observer
12
+ observable(:observable_name) do
13
+ depends_on Order, :user_id, :state
14
+
15
+ handler(Order) do |order, event, changes|
16
+ # This block will be called every time an Order is created, destroyed or updated on user_id or state
17
+ # 'event' will be either insert, delete or update
18
+ # changes will be a hash of changes occurred on the order instance, in the case of an update, ex:
19
+ # `{user_id: [1, 2], state: ["pending", "sent"]}` (pairs of old_value, new_value)
20
+ end
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'wazowski'
31
+ ```
32
+
33
+ And then execute:
34
+
35
+ $ bundle
36
+
37
+ Or install it yourself as:
38
+
39
+ $ gem install wazowski
40
+
41
+
42
+
43
+ ## Why this?
44
+
45
+ - Allows you to put your code in the scope it belongs, not necessarily in the model. Ex: If you're syncing a model
46
+ with third party service, you could setup the observer inside the namespace of this third party (ej `Salesforce`)
47
+ instead of using AR hooks directly in the observed model. It makes it easy to remove the integration with that
48
+ service in the future, it helps you decouple things.
49
+
50
+ - Provides an unified point of control from where to manage data observations and derived logic. Ex: If you want
51
+ to sync data to a third party service, having that source data in 3 different models that should be combined and
52
+ pushed. If all those 3 models change during a transaction, you'll be executing the
53
+ sync 3 times, when you could just do it once. This kind of control is difficult when using AR hooks directly in
54
+ models, but it can be implemented easily with Wazowski (see example use cases at the end).
55
+
56
+ - Abstracts away the particularities about how the data is observed. You write in a declarative way what do you
57
+ want to happen (ej: run this if that changes) instead of directly using AR hooks. Helps in maintaining changes in
58
+ AR itself, or if you want to change your data management strategy (change AR for something else).
59
+
60
+ - Because it's declarative, allows you to work transparently with different data sources (different databases,
61
+ different ORM's, etc) while maintaining a unique view about how to declare data observations.
62
+
63
+
64
+ ## Detailed usage and public API
65
+
66
+ You must start by creating a class inheriting from `Wazowski::Observer`. Then, you can declare observers with the
67
+ `observable` method and syntax described below. The organization of the observers is up to you, you can declare
68
+ many `observable's` in the same class or create multiple different classes, depending on your code organization needs.
69
+
70
+ For every `observable` you must indicate which models and attributes of those models you want to observe. For any
71
+ observed model, then you must provide a handler. This handler will be invoked when a change on that model occurs.
72
+
73
+ You can observe changes on a model in different ways. The first one is manually specifying a list of attributes
74
+ to observe:
75
+
76
+ ```ruby
77
+ observable(:name) do
78
+ depends_on User, :name, :email
79
+
80
+ handler(User) do |obj, event, changes|
81
+ # changes will include the old a new values of name and email on update
82
+ # on creation and deletion, changes will be an empty hash
83
+ end
84
+ end
85
+ ```
86
+
87
+ But you can also choose to observe on any attribute:
88
+
89
+ ```ruby
90
+ observable(:name) do
91
+ depends_on User, :any
92
+
93
+ handler(User) do |obj, event, changes|
94
+ # Since you're observing User on any attribute, changes will be always empty
95
+ end
96
+ end
97
+ ```
98
+
99
+ In this case the handler callback will receive no information about changes, even in update.
100
+
101
+ You can also observe a model's "presence". Your code will be executed only on create and deletion of the dependant
102
+ model, but not on updates:
103
+
104
+ ```ruby
105
+ observable(:name) do
106
+ depends_on User, :none
107
+
108
+ handler(User) do |obj, event, changes|
109
+ # This block will not run on update of users.
110
+ end
111
+ end
112
+ ```
113
+
114
+
115
+ You can also observe more than one model in one observable, in this case you must provide a handler for each model:
116
+
117
+ ```ruby
118
+ observable(:user_sync_to_salesforce) do
119
+ depends_on User, :all
120
+ depends_on Order, :none
121
+ depends_on BillingInfo, :all
122
+
123
+ foo = proc do |obj, event, changes|
124
+ # reused handler
125
+ end
126
+
127
+ handler(User, &foo)
128
+ handler(Order, &foo)
129
+ handler(BillingInfo, &foo)
130
+ # ...
131
+ end
132
+ ```
133
+
134
+ When defining handlers, you can also specify handlers to run only on certain events. You can choose between `:insert`,
135
+ `:update` and `:delete`.
136
+
137
+ ```ruby
138
+ observable(:user_sync_to_salesforce) do
139
+ depends_on User, :none
140
+
141
+ handler(User, only: :insert) do |obj, event|
142
+ # Will run only on create
143
+ end
144
+ end
145
+ ```
146
+
147
+ Your blocks will be executed always on `after_commit`. They will not run if the transaction is rolled back.
148
+ Before triggering your code, the changes that happened inside the transaction will be accumulated
149
+ (ej: insert+delete = noop, insert + update = insert, etc.).
150
+
151
+ Note that if you update attributes on a model inside a handler, this can result in an infinite loop if the attributes
152
+ you're changing are also observed by the same handler. You can, however, "chain" multiple observers (i.e., one
153
+ handler can trigger an update for an attribute observed in a second handler, etc.) as long as this graph
154
+ does not contain cycles.
155
+
156
+
157
+ ### Context of execution for handlers
158
+
159
+ The context in which your handlers will be executed is an instance of the class in which the `observable`
160
+ is defined. This gives you flexibility for reusing common code across your handlers by including your own modules, ex:
161
+
162
+ ```ruby
163
+ class ObserversForSomething < Wazowski::Observer
164
+ include MyCommonObserverLogic
165
+
166
+ observable(:name) do
167
+ # ...
168
+ handler(Model) do |_|
169
+ # use_common_logic_here from MyCommonObserverLogic
170
+ # 'self' == ObserversForSomething.new
171
+ end
172
+ end
173
+ end
174
+ ```
175
+
176
+ The instance used as context will be newly created for every committed transaction occurred.
177
+
178
+ If you're observing multiple models inside an observable, and all those models experience changes inside a transaction
179
+ (say, i.e. 4 models), all your 4 handlers will be executed. Since all those changes occurred in a single transaction,
180
+ the context instance will be the same for the 4 handler executions, so you can use state to implement features.
181
+
182
+ On the other hand, if those 4 models experience changes in 4 isolated transactions, your 4 handlers will be executed
183
+ every time with a newly created context instance.
184
+
185
+ Your observer classes can implement an initialization method, but it must have no arguments in order for Wazowski
186
+ to be able to instantiate them.
187
+
188
+
189
+ ## Usage examples
190
+
191
+ ### Execute a handler only once per transaction
192
+
193
+ If you're implementing a synchronization of data with a third party service, it's a common practice to develop
194
+ an Etl on your side, which reads information on your database and creates a new representation of that data (possibly
195
+ with manipulation logics, like re-formatting of currencies, countries, etc.) that is then pushed to the service.
196
+
197
+ The source data is scattered across multiple models, so you need to watch for all those points to re-sync when
198
+ they change. But since the Etl has only one entry point, in the case all those models are updated inside the same
199
+ transaction you want the Etl to run only once.
200
+
201
+ To accomplish this, you can use some state inside your observer class to run the handler only on the first time:
202
+
203
+ ```ruby
204
+ class MyObserver < Wazowski::Observer
205
+ def initialize
206
+ @counter = 0
207
+ end
208
+
209
+ def run_only_once
210
+ return unless @counter == 0
211
+ yield
212
+ @counter += 1
213
+ end
214
+
215
+ observable(:user_sync_to_salesforce) do
216
+ depends_on User, :all
217
+ depends_on Order, :none
218
+ depends_on BillingInfo, :all
219
+
220
+ handler(User) { |user|
221
+ run_only_once { Etl.run(user) }
222
+ }
223
+ handler(Order) { |order|
224
+ run_only_once { Etl.run(order.user) }
225
+ }
226
+ handler(BillingInfo) { |billing_info|
227
+ run_only_once { Etl.run(billing_info.user) }
228
+ }
229
+ end
230
+ end
231
+ ```
232
+
233
+
234
+ ## Internal details
235
+
236
+ This tool currently works only with ActiveRecord, but it's design accepts multiple adapters if you ever want
237
+ to write a new one.
238
+
239
+ The public API currently supports only relational info, but it could be expanded in the future to support
240
+ other types of data (graph, key/value, etc.) maintaining backwards compatibility.
241
+
242
+ Even tough the current public API works by specifying directly models (ej: `User`) this class is never used
243
+ internally and is only forwarded to the adapter. To use this library with a repository pattern, the same API
244
+ could be used but instead of providing AR Classes your could provide Repository classes. However, this is only
245
+ an idea, no attempt to use this with a repository pattern has been tried.
246
+
247
+
248
+ ### Adapter's API
249
+
250
+ - An adapter is the responsible to actually observe the data. It will receive the information provided by the
251
+ user about what data has to be observed (models, attributes) and it will be responsible of calling Wazowski's hooks
252
+ whenever a database commit happens.
253
+
254
+ - An adapter is a module or class.
255
+
256
+ - An adapter must implement a `register_node` class method. This method receives 1) a node_id (string) and
257
+ 2) a hash of Class => list_of_attributes (as symbols) as defined in the public DSL. The adapter must use this information to
258
+ prepare its setup. It must keep the `node_id` string to reuse it. An attribute can also be `:none` or `:any`.
259
+ For `:none`, the observer indicates it only wants to receive creation and deletion callbacks. For `:any`, the observer
260
+ indicates it wants to receive all callbacks, including updates on any attribute, but no need to pass specific
261
+ dirty info.
262
+
263
+ - The adapter must call the `Wazowski.run_handlers(changes_per_node)` every time a transaction is committed,
264
+ once and only once per transaction even if multiple models have changed inside the transaction. It must provide as
265
+ an argument the accumulated information of changes occurred per node inside the transaction. This data structure
266
+ is a hash where each key is a node_id (as provided in the `register_node` call) and each value is an array of
267
+ changes occurred on that node. A change is defined as an array of 3 elements: the change type, the class and the
268
+ object. Example:
269
+
270
+ ```ruby
271
+ {
272
+ "TestObserver/valid_comments_count"=>[[:insert, Post, #<Post id: 1, ...>]],
273
+ "TestObserver/only_on_insert"=>[[:insert, Post, #<Post id: 1, ...>]]}
274
+ }
275
+ ```
276
+
277
+ - The adapter must behave in a transactional way, i.e., if a record is created and deleted inside a transaction, no
278
+ callback should be called whatsoever.
279
+
280
+
281
+ ## Development
282
+
283
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
284
+
285
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
286
+
287
+ ## Contributing
288
+
289
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rogercampos/wazowski.
290
+
291
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "wazowski"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/module'
5
+ require 'active_record'
6
+
7
+ require "wazowski/version"
8
+ require 'wazowski/active_record_adapter'
9
+
10
+ module Wazowski
11
+ NoSuchNode = Class.new(StandardError)
12
+ ConfigurationError = Class.new(StandardError)
13
+
14
+ module Observable
15
+ extend ActiveSupport::Concern
16
+
17
+ class_methods do
18
+ def observable(name, &block)
19
+ id = "#{self.name || object_id}/#{name}"
20
+
21
+ node = Node.new(id, self, block)
22
+ Config.derivations[id] = node
23
+
24
+ ActiveRecordAdapter.register_node(id, node.dependants)
25
+ end
26
+ end
27
+ end
28
+
29
+ class Observer
30
+ include Observable
31
+ end
32
+
33
+ module Config
34
+ mattr_accessor :derivations
35
+ self.derivations = {}
36
+ end
37
+
38
+ class Node
39
+ attr_reader :name
40
+
41
+ def initialize(name, klass, block)
42
+ @name = name
43
+ @observer_klass = klass
44
+ instance_eval(&block)
45
+ end
46
+
47
+ def depends_on(klass, *attrs)
48
+ if attrs.empty?
49
+ raise ConfigurationError, 'Must depend on some attributes. You can also use `:none` and `:any`'
50
+ end
51
+
52
+ @depends_on ||= {}
53
+ @depends_on[klass] ||= []
54
+ @depends_on[klass] += attrs
55
+ end
56
+
57
+ def handler(klass, opts = {}, &block)
58
+ @handlers ||= {}
59
+ raise ConfigurationError, "Already defined handler for #{klass}" if @handlers[klass]
60
+
61
+ @handlers[klass] = { block: block, opts: opts }
62
+ end
63
+
64
+ def dependants
65
+ @depends_on
66
+ end
67
+
68
+ def inspect
69
+ "<Wazowski::Node #{@name}>"
70
+ end
71
+
72
+ def lookup_handler(klass)
73
+ handler = if @handlers[klass]
74
+ @handlers[klass]
75
+ else
76
+ lookup_handler(klass.superclass) unless klass.superclass == Object
77
+ end
78
+
79
+ if handler.nil?
80
+ raise(ConfigurationError, "Cannot run handler for klass #{klass}, it has been never "\
81
+ 'registered! Check your definitions.')
82
+ end
83
+
84
+ handler
85
+ end
86
+
87
+ def wrapping
88
+ @context = @observer_klass.new
89
+
90
+ yield
91
+ end
92
+
93
+ def after_commit_on_update(klass, object, dirty_changes)
94
+ raise ConfigurationError, "Needs to be called within a #wrapping call!" if @context.nil?
95
+ handler = lookup_handler(klass)
96
+
97
+ return unless handler[:opts][:only].nil? || [handler[:opts][:only]].flatten.include?(:update)
98
+ @context.instance_exec(object, :update, dirty_changes, &handler[:block])
99
+ end
100
+
101
+ def after_commit_on_delete(klass, object)
102
+ raise ConfigurationError, "Needs to be called within a #wrapping call!" if @context.nil?
103
+ handler = lookup_handler(klass)
104
+
105
+ return unless handler[:opts][:only].nil? || [handler[:opts][:only]].flatten.include?(:delete)
106
+ @context.instance_exec(object, :delete, {}, &handler[:block])
107
+ end
108
+
109
+ def after_commit_on_create(klass, object)
110
+ raise ConfigurationError, "Needs to be called within a #wrapping call!" if @context.nil?
111
+ handler = lookup_handler(klass)
112
+
113
+ return unless handler[:opts][:only].nil? || [handler[:opts][:only]].flatten.include?(:insert)
114
+ @context.instance_exec(object, :insert, {}, &handler[:block])
115
+ end
116
+ end
117
+
118
+ class << self
119
+ def find_node(node_id)
120
+ Config.derivations[node_id] || raise(NoSuchNode, "Node not found! #{node_id}")
121
+ end
122
+
123
+ def run_handlers(changes_per_node)
124
+ changes_per_node.each do |node_id, changes|
125
+ node = find_node(node_id)
126
+
127
+ node.wrapping do
128
+ changes.each do |change_type, klass, object, changeset|
129
+ case change_type
130
+ when :insert
131
+ node.after_commit_on_create(klass, object)
132
+ when :delete
133
+ node.after_commit_on_delete(klass, object)
134
+ when :update
135
+ node.after_commit_on_update(klass, object, changeset)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
@@ -0,0 +1,204 @@
1
+ module Wazowski
2
+ module ActiveRecordAdapter
3
+ module TransactionState
4
+ class StateData
5
+ def initialize
6
+ @changed_models = Set.new
7
+ end
8
+
9
+ def run_after_commit_only_once!
10
+ return if @changed_models.empty?
11
+
12
+ info = {}
13
+
14
+ # even @changed_models being a Set, it's possible that the "same" model
15
+ # have been included in the set before and after persistence (so they're different,
16
+ # we have both in the set) and by the time this code runs, the non persisted model
17
+ # becomes persisted and now they're identical, but still in the set.
18
+ # So we may have duplicates. `.to_a.uniq` to remove them.
19
+ @changed_models.to_a.uniq.each do |model|
20
+ info.merge!(model.__wazowski_changes_per_node) do |_, old_val, new_val|
21
+ old_val + new_val
22
+ end
23
+ end
24
+
25
+ clear_after_commit_performed!
26
+
27
+ Wazowski.run_handlers(info)
28
+ end
29
+
30
+ def clear_after_commit_performed!
31
+ @changed_models.clear
32
+ end
33
+
34
+ def register_model_changed(model)
35
+ @changed_models << model
36
+ end
37
+ end
38
+
39
+ mattr_accessor :states
40
+ self.states = {}
41
+
42
+ def self.current_state
43
+ states[ActiveRecord::Base.connection.hash] ||= StateData.new
44
+ end
45
+ end
46
+
47
+ module WatchDog
48
+ extend ActiveSupport::Concern
49
+
50
+ included do
51
+ cattr_accessor :__wazowski_tracked_attrs, :__wazowski_tracked_base, :__wazowski_tracked_any
52
+ self.__wazowski_tracked_attrs = {}
53
+ self.__wazowski_tracked_base = Set.new
54
+ self.__wazowski_tracked_any = Set.new
55
+
56
+ before_update(append: true) do
57
+ self.class.__wazowski_tracked_any.each do |node_id|
58
+ __wazowski_presence_state.push([:update, node_id])
59
+ TransactionState.current_state.register_model_changed(self)
60
+ end
61
+
62
+ self.class.__wazowski_tracked_attrs.each do |attr, node_ids|
63
+ if send("#{attr}_changed?")
64
+ node_ids.each { |node_id| __wazowski_store_dirty!(attr, node_id, :update, send("#{attr}_was")) }
65
+ end
66
+ end
67
+ end
68
+
69
+ before_destroy do
70
+ self.class.__wazowski_all_nodes.each do |node_id|
71
+ __wazowski_presence_state.push([:delete, node_id])
72
+ TransactionState.current_state.register_model_changed(self)
73
+ end
74
+ end
75
+
76
+ before_create do
77
+ self.class.__wazowski_all_nodes.each do |node_id|
78
+ __wazowski_presence_state.push([:insert, node_id])
79
+ TransactionState.current_state.register_model_changed(self)
80
+ end
81
+ end
82
+
83
+ after_commit do
84
+ TransactionState.current_state.run_after_commit_only_once!
85
+
86
+ __wazowski_clean_dirty!
87
+ __wazowski_presence_state.clear
88
+ end
89
+
90
+ after_rollback do
91
+ __wazowski_clean_dirty!
92
+ __wazowski_presence_state.clear
93
+
94
+ TransactionState.current_state.clear_after_commit_performed!
95
+ end
96
+
97
+ def __wazowski_store_dirty!(attr, node_id, change_type, attr_was = nil)
98
+ __wazowski_dirty_state[attr] ||= {}
99
+ __wazowski_dirty_state[attr][:change_type] = change_type
100
+ __wazowski_dirty_state[attr][:dirty] = attr_was if __wazowski_dirty_state[attr][:dirty].nil?
101
+ __wazowski_dirty_state[attr][:node_ids] ||= Set.new
102
+ __wazowski_dirty_state[attr][:node_ids] << node_id
103
+
104
+ TransactionState.current_state.register_model_changed(self)
105
+ end
106
+
107
+ def __wazowski_changes_per_node
108
+ info = {}
109
+
110
+ states = __wazowski_presence_state.map(&:first)
111
+
112
+ unless states.include?(:insert) || states.include?(:delete)
113
+ changes_by_node = {}
114
+
115
+ __wazowski_dirty_state.each do |attr, data|
116
+ node_ids = data[:node_ids]
117
+ new_data = data.except(:node_ids).merge(attr: attr)
118
+
119
+ node_ids.each do |node_id|
120
+ changes_by_node[node_id] ||= []
121
+ changes_by_node[node_id] << new_data
122
+ end
123
+ end
124
+
125
+ changes_by_node.each do |node_id, datas|
126
+ list_of_changes = datas.map { |x| [x[:attr], [x[:dirty], send(x[:attr])]] }.to_h
127
+
128
+ info[node_id] ||= []
129
+ info[node_id] << [:update, self.class, self, list_of_changes]
130
+ end
131
+ end
132
+
133
+ unless states.include?(:insert) && states.include?(:delete)
134
+ __wazowski_presence_state.each do |type, node_id|
135
+ info[node_id] ||= []
136
+
137
+ case type
138
+ when :insert
139
+ info[node_id] << [:insert, self.class, self]
140
+ when :delete
141
+ info[node_id] << [:delete, self.class, self]
142
+ when :update
143
+ info[node_id] << [:update, self.class, self, {}]
144
+ end
145
+ end
146
+ end
147
+
148
+ info
149
+ end
150
+
151
+ def __wazowski_clean_dirty!
152
+ @__wazowski_dirty_state = {}
153
+ end
154
+
155
+ def __wazowski_dirty_state
156
+ @__wazowski_dirty_state ||= {}
157
+ end
158
+
159
+ def __wazowski_presence_state
160
+ @__wazowski_presence_state ||= []
161
+ end
162
+ end
163
+
164
+ class_methods do
165
+ def __wazowski_track_on!(attr, node_id)
166
+ __wazowski_tracked_attrs[attr] ||= Set.new
167
+ __wazowski_tracked_attrs[attr] << node_id
168
+ end
169
+
170
+ def __wazowski_track_base!(node_id)
171
+ __wazowski_tracked_base << node_id
172
+ end
173
+
174
+ def __wazowski_track_any!(node_id)
175
+ __wazowski_tracked_any << node_id
176
+ end
177
+
178
+ def __wazowski_all_nodes
179
+ nodes_by_attribute = __wazowski_tracked_attrs.values
180
+
181
+ (nodes_by_attribute.any? ? nodes_by_attribute.sum : Set.new) +
182
+ __wazowski_tracked_base +
183
+ __wazowski_tracked_any
184
+ end
185
+ end
186
+ end
187
+
188
+ def self.register_node(node_id, observed_hierarchy)
189
+ observed_hierarchy.each do |klass, attrs|
190
+ klass.send(:include, WatchDog) unless klass.included_modules.include?(WatchDog)
191
+
192
+ if attrs.size == 1 && attrs[0] == :none
193
+ klass.__wazowski_track_base!(node_id)
194
+ elsif attrs.size == 1 && attrs[0] == :any
195
+ klass.__wazowski_track_any!(node_id)
196
+ else
197
+ attrs.each do |attr|
198
+ klass.__wazowski_track_on!(attr, node_id)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,3 @@
1
+ module Wazowski
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,39 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "wazowski/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "wazowski"
7
+ spec.version = Wazowski::VERSION
8
+ spec.authors = ["Roger Campos"]
9
+ spec.email = ["roger@rogercampos.com"]
10
+
11
+ spec.summary = %q{declarative active record data observers}
12
+ spec.description = %q{declarative active record data observers}
13
+ spec.homepage = "https://github.com/rogercampos/wazowski"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against " \
21
+ "public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "activerecord", ">= 4.2"
32
+ spec.add_dependency "activesupport", ">= 4.2"
33
+
34
+ spec.add_development_dependency "bundler", "~> 1.16"
35
+ spec.add_development_dependency "rake", "~> 10.0"
36
+ spec.add_development_dependency "minitest", "~> 5.0"
37
+ spec.add_development_dependency "pg"
38
+ spec.add_development_dependency "dotenv"
39
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wazowski
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Roger Campos
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dotenv
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: declarative active record data observers
112
+ email:
113
+ - roger@rogercampos.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - Gemfile
121
+ - Gemfile.lock
122
+ - README.md
123
+ - Rakefile
124
+ - bin/console
125
+ - bin/setup
126
+ - lib/wazowski.rb
127
+ - lib/wazowski/active_record_adapter.rb
128
+ - lib/wazowski/version.rb
129
+ - wazowski.gemspec
130
+ homepage: https://github.com/rogercampos/wazowski
131
+ licenses: []
132
+ metadata:
133
+ allowed_push_host: https://rubygems.org
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.7.3
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: declarative active record data observers
154
+ test_files: []