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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +46 -0
- data/README.md +291 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/wazowski.rb +143 -0
- data/lib/wazowski/active_record_adapter.rb +204 -0
- data/lib/wazowski/version.rb +3 -0
- data/wazowski.gemspec +39 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -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
data/Gemfile.lock
ADDED
@@ -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
|
data/README.md
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
[](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
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/lib/wazowski.rb
ADDED
@@ -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
|
data/wazowski.gemspec
ADDED
@@ -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: []
|