wazowski 0.1.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.
@@ -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: []