u-observers 0.6.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/.travis.sh +34 -0
- data/.travis.yml +27 -3
- data/Gemfile +31 -5
- data/README.md +369 -11
- data/lib/micro/observers.rb +3 -23
- data/lib/micro/observers/event.rb +21 -0
- data/lib/micro/observers/event/names.rb +27 -0
- data/lib/micro/observers/for/active_model.rb +36 -0
- data/lib/micro/observers/for/active_record.rb +16 -0
- data/lib/micro/observers/set.rb +176 -0
- data/lib/micro/observers/utils.rb +1 -0
- data/lib/micro/observers/version.rb +1 -1
- data/lib/u-observers/for/active_model.rb +2 -0
- data/lib/u-observers/for/active_record.rb +2 -0
- data/test.sh +11 -0
- data/u-observers.gemspec +5 -1
- metadata +41 -5
- data/lib/micro/observers/events_or_actions.rb +0 -25
- data/lib/micro/observers/manager.rb +0 -87
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5098d44090f611866c96aa4005c6475742972a9a986dbbcd5618753932b33817
|
4
|
+
data.tar.gz: 8b18f175cc5ff7fd1aae8e34302be62a1f295a151231c62e253d4b984d0225b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e51002b6af05c7aba17eff30ba962d1d726ce9e169ae19c8a71626c035daf7c1e4e6da727c74f73c06c46ad0f5b1faaad1d1248df018ada97aa123a613e1dedf
|
7
|
+
data.tar.gz: 20972cd57db02b9a25905896af9429a597cfb22e951fe73a3af6346962cd1f3574df30c241b7aa9ef3a99c502097ad0a7d75210a33227a98c6d3435371c8bb03
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.5
|
data/.travis.sh
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
ruby_v=$(ruby -v)
|
4
|
+
|
5
|
+
bundle update
|
6
|
+
bundle exec rake test
|
7
|
+
|
8
|
+
ACTIVERECORD_VERSION='3.2' bundle update
|
9
|
+
ACTIVERECORD_VERSION='3.2' bundle exec rake test
|
10
|
+
|
11
|
+
ACTIVERECORD_VERSION='4.0' bundle update
|
12
|
+
ACTIVERECORD_VERSION='4.0' bundle exec rake test
|
13
|
+
|
14
|
+
ACTIVERECORD_VERSION='4.1' bundle update
|
15
|
+
ACTIVERECORD_VERSION='4.1' bundle exec rake test
|
16
|
+
|
17
|
+
ACTIVERECORD_VERSION='4.2' bundle update
|
18
|
+
ACTIVERECORD_VERSION='4.2' bundle exec rake test
|
19
|
+
|
20
|
+
ACTIVERECORD_VERSION='5.0' bundle update
|
21
|
+
ACTIVERECORD_VERSION='5.0' bundle exec rake test
|
22
|
+
|
23
|
+
ACTIVERECORD_VERSION='5.1' bundle update
|
24
|
+
ACTIVERECORD_VERSION='5.1' bundle exec rake test
|
25
|
+
|
26
|
+
if [[ ! $ruby_v =~ '2.2.0' ]]; then
|
27
|
+
ACTIVERECORD_VERSION='5.2' bundle update
|
28
|
+
ACTIVERECORD_VERSION='5.2' bundle exec rake test
|
29
|
+
fi
|
30
|
+
|
31
|
+
if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
|
32
|
+
ACTIVERECORD_VERSION='6.0' bundle update
|
33
|
+
ACTIVERECORD_VERSION='6.0' bundle exec rake test
|
34
|
+
fi
|
data/.travis.yml
CHANGED
@@ -1,6 +1,30 @@
|
|
1
1
|
---
|
2
2
|
language: ruby
|
3
|
-
|
3
|
+
|
4
|
+
sudo: false
|
5
|
+
|
4
6
|
rvm:
|
5
|
-
|
6
|
-
|
7
|
+
- 2.2.0
|
8
|
+
- 2.3.0
|
9
|
+
- 2.4.0
|
10
|
+
- 2.5.0
|
11
|
+
- 2.6.0
|
12
|
+
- 2.7.0
|
13
|
+
|
14
|
+
cache: bundler
|
15
|
+
|
16
|
+
before_install:
|
17
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
18
|
+
- gem install bundler -v '< 2'
|
19
|
+
|
20
|
+
install: bundle install --jobs=3 --retry=3
|
21
|
+
|
22
|
+
before_script:
|
23
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
24
|
+
- chmod +x ./cc-test-reporter
|
25
|
+
- "./cc-test-reporter before-build"
|
26
|
+
|
27
|
+
script: "./.travis.sh"
|
28
|
+
|
29
|
+
after_success:
|
30
|
+
- "./cc-test-reporter after-build -t simplecov"
|
data/Gemfile
CHANGED
@@ -3,12 +3,38 @@ source 'https://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in u-observers.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '6.1')
|
7
|
+
|
8
|
+
activerecord = case activerecord_version
|
9
|
+
when '3.2' then '3.2.22'
|
10
|
+
when '4.0' then '4.0.13'
|
11
|
+
when '4.1' then '4.1.16'
|
12
|
+
when '4.2' then '4.2.11'
|
13
|
+
when '5.0' then '5.0.7'
|
14
|
+
when '5.1' then '5.1.7'
|
15
|
+
when '5.2' then '5.2.3'
|
16
|
+
when '6.0' then '6.0.3'
|
17
|
+
end
|
18
|
+
|
19
|
+
simplecov_version =
|
20
|
+
case RUBY_VERSION
|
21
|
+
when /\A2.[23]/ then '~> 0.17.1'
|
22
|
+
when /\A2.4/ then '~> 0.18.5'
|
23
|
+
else '~> 0.19'
|
24
|
+
end
|
8
25
|
|
9
26
|
group :test do
|
10
|
-
gem '
|
11
|
-
gem '
|
27
|
+
gem 'minitest', activerecord_version < '4.1' ? '~> 4.2' : '~> 5.0'
|
28
|
+
gem 'simplecov', simplecov_version, require: false
|
29
|
+
|
30
|
+
if activerecord
|
31
|
+
sqlite3 =
|
32
|
+
case activerecord
|
33
|
+
when /\A6\.0/, nil then '~> 1.4.0'
|
34
|
+
else '~> 1.3.0'
|
35
|
+
end
|
12
36
|
|
13
|
-
|
37
|
+
gem 'sqlite3', sqlite3
|
38
|
+
gem 'activerecord', activerecord, require: 'active_record'
|
39
|
+
end
|
14
40
|
end
|
data/README.md
CHANGED
@@ -1,28 +1,386 @@
|
|
1
|
-
|
1
|
+
<p align="center">
|
2
|
+
<h1 align="center">👀 μ-observers</h1>
|
3
|
+
<p align="center"><i>Simple and powerful implementation of the observer pattern.</i></p>
|
4
|
+
<br>
|
5
|
+
</p>
|
2
6
|
|
3
|
-
|
7
|
+
<p align="center">
|
8
|
+
<img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
4
9
|
|
5
|
-
|
10
|
+
<a href="https://rubygems.org/gems/u-observers">
|
11
|
+
<img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
|
12
|
+
</a>
|
6
13
|
|
7
|
-
|
14
|
+
<a href="https://travis-ci.com/serradura/u-observers">
|
15
|
+
<img alt="Build Status" src="https://travis-ci.com/serradura/u-observers.svg?branch=main">
|
16
|
+
</a>
|
8
17
|
|
9
|
-
|
18
|
+
<a href="https://codeclimate.com/github/serradura/u-observers/maintainability">
|
19
|
+
<img alt="Maintainability" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/maintainability">
|
20
|
+
</a>
|
21
|
+
|
22
|
+
<a href="https://codeclimate.com/github/serradura/u-observers/test_coverage">
|
23
|
+
<img alt="Test Coverage" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/test_coverage">
|
24
|
+
</a>
|
25
|
+
</p>
|
26
|
+
|
27
|
+
This gem implements the observer pattern [[1]](https://en.wikipedia.org/wiki/Observer_pattern)[[2]](https://refactoring.guru/design-patterns/observer) (also known as publish/subscribe). It provides a simple mechanism for one object to inform a set of interested third-party objects when its state changes.
|
28
|
+
|
29
|
+
Ruby's standard library [has an abstraction](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html) that enables you to use this pattern. But its design can conflict with other mainstream libraries, like the [`ActiveModel`/`ActiveRecord`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changed), which also has the [`changed`](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html#method-i-changed) method. In this case, the behavior of the Stdlib will be been compromised.
|
30
|
+
|
31
|
+
Because of this issue, I decided to create a gem that encapsulates the pattern without changing the object's implementation so much. The `Micro::Observers` includes just one instance method in the target class (its instance will be the observed subject).
|
32
|
+
|
33
|
+
# Table of contents <!-- omit in toc -->
|
34
|
+
- [Installation](#installation)
|
35
|
+
- [Compatibility](#compatibility)
|
36
|
+
- [Usage](#usage)
|
37
|
+
- [Passing a context for your observers](#passing-a-context-for-your-observers)
|
38
|
+
- [Passing data when performing observers](#passing-data-when-performing-observers)
|
39
|
+
- [What is a `Micro::Observers::Event`?](#what-is-a-microobserversevent)
|
40
|
+
- [Passing a callable as an observer](#passing-a-callable-as-an-observer)
|
41
|
+
- [Calling the observers](#calling-the-observers)
|
42
|
+
- [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
|
43
|
+
- [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
|
44
|
+
- [notify_observers_on()](#notify_observers_on)
|
45
|
+
- [notify_observers()](#notify_observers)
|
46
|
+
- [Development](#development)
|
47
|
+
- [Contributing](#contributing)
|
48
|
+
- [License](#license)
|
49
|
+
- [Code of Conduct](#code-of-conduct)
|
50
|
+
|
51
|
+
# Installation
|
52
|
+
|
53
|
+
Add this line to your application's Gemfile and `bundle install`:
|
10
54
|
|
11
55
|
```ruby
|
12
|
-
gem '
|
56
|
+
gem 'u-observers'
|
13
57
|
```
|
14
58
|
|
15
|
-
|
59
|
+
# Compatibility
|
16
60
|
|
17
|
-
|
61
|
+
| u-observers | branch | ruby | activerecord |
|
62
|
+
| ----------- | ------- | -------- | ------------- |
|
63
|
+
| 2.0.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
|
64
|
+
| 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
18
65
|
|
19
|
-
|
66
|
+
> **Note**: The ActiveRecord isn't a dependency, but you could add a module to enable some static methods that were designed to be used with its [callbacks](https://guides.rubyonrails.org/active_record_callbacks.html).
|
20
67
|
|
21
|
-
|
68
|
+
[⬆️ Back to Top](#table-of-contents-)
|
22
69
|
|
23
70
|
## Usage
|
24
71
|
|
25
|
-
|
72
|
+
Any class with `Micro::Observers` module included can notify events to attached observers.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
require 'securerandom'
|
76
|
+
|
77
|
+
class Order
|
78
|
+
include Micro::Observers
|
79
|
+
|
80
|
+
attr_reader :code
|
81
|
+
|
82
|
+
def initialize
|
83
|
+
@code, @status = SecureRandom.alphanumeric, :draft
|
84
|
+
end
|
85
|
+
|
86
|
+
def canceled?
|
87
|
+
@status == :canceled
|
88
|
+
end
|
89
|
+
|
90
|
+
def cancel!
|
91
|
+
return self if canceled?
|
92
|
+
|
93
|
+
@status = :canceled
|
94
|
+
|
95
|
+
observers.subject_changed!
|
96
|
+
observers.notify(:canceled) and return self
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module OrderEvents
|
101
|
+
def self.canceled(order)
|
102
|
+
puts "The order #(#{order.code}) has been canceled."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
order = Order.new
|
107
|
+
#<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
|
108
|
+
|
109
|
+
order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
|
110
|
+
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]
|
111
|
+
|
112
|
+
order.canceled?
|
113
|
+
# false
|
114
|
+
|
115
|
+
order.cancel!
|
116
|
+
# The message below will be printed by the observer (OrderEvents):
|
117
|
+
# The order #(X0o9yf1GsdQFvLR4) has been canceled
|
118
|
+
|
119
|
+
order.canceled?
|
120
|
+
# true
|
121
|
+
|
122
|
+
order.observers.detach(OrderEvents) # detaching multiple observers. e.g. observers.detach(A, B, C)
|
123
|
+
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]
|
124
|
+
|
125
|
+
order.canceled?
|
126
|
+
# true
|
127
|
+
|
128
|
+
order.observers.subject_changed!
|
129
|
+
order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.
|
130
|
+
```
|
131
|
+
|
132
|
+
**Highlights of the previous example:**
|
133
|
+
|
134
|
+
To avoid an undesired behavior, do you need to mark the subject as changed before notify your observers about some event.
|
135
|
+
|
136
|
+
You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
|
137
|
+
|
138
|
+
But if you need to apply some conditional to mark a change, you can use the `#subject_changed` method. e.g. `observers.subject_changed(name != new_name)`
|
139
|
+
|
140
|
+
The `#notify` method always requires an event to make a broadcast. So, if you try to use it without one or more events (symbol values) you will get an exception.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
order.observers.notify
|
144
|
+
# ArgumentError (no events (expected at least 1))
|
145
|
+
```
|
146
|
+
|
147
|
+
[⬆️ Back to Top](#table-of-contents-)
|
148
|
+
|
149
|
+
### Passing a context for your observers
|
150
|
+
|
151
|
+
To pass a context (any kind of Ruby object) for one or more observers, you will need to use the `context:` keyword as the last argument of the `#attach` method.
|
152
|
+
|
153
|
+
When the observer method receives two arguments, the first one will be the subject, and the second one an instance of `Micro::Observers::Event`.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class Order
|
157
|
+
include Micro::Observers
|
158
|
+
|
159
|
+
def cancel!
|
160
|
+
observers.subject_changed!
|
161
|
+
observers.notify(:canceled)
|
162
|
+
self
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
module OrderEvents
|
167
|
+
def self.canceled(order, event)
|
168
|
+
puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
order = Order.new
|
173
|
+
order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
|
174
|
+
order.cancel!
|
175
|
+
# The message below will be printed by the observer (OrderEvents):
|
176
|
+
# The order #(70196221441820) has been canceled. (from: example #2)
|
177
|
+
```
|
178
|
+
|
179
|
+
[⬆️ Back to Top](#table-of-contents-)
|
180
|
+
|
181
|
+
### Passing data when performing observers
|
182
|
+
|
183
|
+
The [`event context`](#passing-a-context-for-your-observers) is a value that is stored when you attach your observer. But sometimes, will be useful to send some additional data when broadcasting an event to the observers.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class Order
|
187
|
+
include Micro::Observers
|
188
|
+
end
|
189
|
+
|
190
|
+
module OrderHandler
|
191
|
+
def self.changed(order, event)
|
192
|
+
puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
order = Order.new
|
197
|
+
order.observers.attach(OrderHandler, context: { from: 'example #3' })
|
198
|
+
order.observers.subject_changed!
|
199
|
+
order.observers.notify(:changed, data: 1)
|
200
|
+
# The message below will be printed by the observer (OrderHandler):
|
201
|
+
# The order #(70196221441820) received the number 1 from example #3.
|
202
|
+
```
|
203
|
+
|
204
|
+
[⬆️ Back to Top](#table-of-contents-)
|
205
|
+
|
206
|
+
### What is a `Micro::Observers::Event`?
|
207
|
+
|
208
|
+
The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
|
209
|
+
|
210
|
+
- `#name` will be the broadcasted event.
|
211
|
+
- `#subject` will be the observed subject.
|
212
|
+
- `#context` will be [the context data](#passing-a-context-for-your-observers) that was attached to the observer.
|
213
|
+
- `#data` will be [the value that was passed to the observers' notification](#passing-data-when-performing-observers).
|
214
|
+
|
215
|
+
[⬆️ Back to Top](#table-of-contents-)
|
216
|
+
|
217
|
+
### Passing a callable as an observer
|
218
|
+
|
219
|
+
The `observers.on()` method enables you to attach callable as observers. It could receive three options:
|
220
|
+
1. `:event` that will be notified
|
221
|
+
2. `:call` with the callable object.
|
222
|
+
3. `:with` (optional) it can define the value which will be used as the callable object's argument. So, if it receives a `Proc` a `Micro::Observers::Event` instance will be passed to it and the argument will be defined as the `Proc` output. But if this option wasn't be defined, the `Micro::Observers::Event` instance will be its argument.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class Person
|
226
|
+
include Micro::Observers
|
227
|
+
|
228
|
+
attr_reader :name
|
229
|
+
|
230
|
+
def initialize(name)
|
231
|
+
@name = name
|
232
|
+
end
|
233
|
+
|
234
|
+
def name=(new_name)
|
235
|
+
observers.subject_changed(new_name != @name)
|
236
|
+
|
237
|
+
return unless observers.subject_changed?
|
238
|
+
|
239
|
+
@name = new_name
|
240
|
+
|
241
|
+
observers.notify(:name_has_been_changed)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
PrintPersonName = -> (data) do
|
246
|
+
puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
|
247
|
+
end
|
248
|
+
|
249
|
+
person = Person.new('Rodrigo')
|
250
|
+
|
251
|
+
person.observers.on(
|
252
|
+
event: :name_has_been_changed,
|
253
|
+
call: PrintPersonName,
|
254
|
+
with: -> event { {person: event.subject, number: rand} }
|
255
|
+
)
|
256
|
+
|
257
|
+
person.name = 'Serradura'
|
258
|
+
# The message below will be printed by the observer (PrintPersonName):
|
259
|
+
# Person name: Serradura, number: 0.5018509191706862
|
260
|
+
```
|
261
|
+
|
262
|
+
[⬆️ Back to Top](#table-of-contents-)
|
263
|
+
|
264
|
+
### Calling the observers
|
265
|
+
|
266
|
+
You can use a callable (a class, module, or object that responds to the call method) to be your observers.
|
267
|
+
To do this, you only need make use of the method `#call` instead of `#notify`.
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
class Order
|
271
|
+
include Micro::Observers
|
272
|
+
|
273
|
+
def cancel!
|
274
|
+
observers.subject_changed!
|
275
|
+
observers.call # in practice, this is a shortcut to observers.notify(:call)
|
276
|
+
self
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
|
281
|
+
|
282
|
+
order = Order.new
|
283
|
+
order.observers.attach(OrderCancellation)
|
284
|
+
order.cancel!
|
285
|
+
# The message below will be printed by the observer (OrderCancellation):
|
286
|
+
# The order #(70196221441820) has been canceled.
|
287
|
+
```
|
288
|
+
|
289
|
+
> **Note**: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.a
|
290
|
+
|
291
|
+
[⬆️ Back to Top](#table-of-contents-)
|
292
|
+
|
293
|
+
### Notifying observers without marking them as changed
|
294
|
+
|
295
|
+
This feature needs to be used with caution!
|
296
|
+
|
297
|
+
If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
|
298
|
+
|
299
|
+
[⬆️ Back to Top](#table-of-contents-)
|
300
|
+
|
301
|
+
### ActiveRecord and ActiveModel integrations
|
302
|
+
|
303
|
+
To make use of this feature you need to require an additional module (`require 'u-observers/for/active_record'`).
|
304
|
+
|
305
|
+
Gemfile example:
|
306
|
+
```ruby
|
307
|
+
gem 'u-observers', require: 'u-observers/for/active_record'
|
308
|
+
```
|
309
|
+
|
310
|
+
This feature will expose modules that could be used to add macros (static methods) that were designed to work with `ActiveModel`/`ActiveRecord` callbacks. e.g:
|
311
|
+
|
312
|
+
|
313
|
+
#### notify_observers_on()
|
314
|
+
|
315
|
+
The `notify_observers_on` allows you to pass one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class Post < ActiveRecord::Base
|
319
|
+
include ::Micro::Observers::For::ActiveRecord
|
320
|
+
|
321
|
+
notify_observers_on(:after_commit) # passing multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
|
322
|
+
end
|
323
|
+
|
324
|
+
module TitlePrinter
|
325
|
+
def self.after_commit(post)
|
326
|
+
puts "Title: #{post.title}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
module TitlePrinterWithContext
|
331
|
+
def self.after_commit(post, event)
|
332
|
+
puts "Title: #{post.title} (from: #{event.context[:from]})"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
Post.transaction do
|
337
|
+
post = Post.new(title: 'Hello world')
|
338
|
+
post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
|
339
|
+
post.save
|
340
|
+
end
|
341
|
+
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
342
|
+
# Title: Hello world
|
343
|
+
# Title: Hello world (from: example #6)
|
344
|
+
```
|
345
|
+
|
346
|
+
[⬆️ Back to Top](#table-of-contents-)
|
347
|
+
|
348
|
+
#### notify_observers()
|
349
|
+
|
350
|
+
The `notify_observers` allows you to pass one or more *events*, that will be used to notify after the execution of some `ActiveModel`/`ActiveRecord` callback.
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
class Post < ActiveRecord::Base
|
354
|
+
include ::Micro::Observers::For::ActiveRecord
|
355
|
+
|
356
|
+
after_commit(¬ify_observers(:transaction_completed))
|
357
|
+
end
|
358
|
+
|
359
|
+
module TitlePrinter
|
360
|
+
def self.transaction_completed(post)
|
361
|
+
puts("Title: #{post.title}")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
module TitlePrinterWithContext
|
366
|
+
def self.transaction_completed(post, event)
|
367
|
+
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
Post.transaction do
|
372
|
+
post = Post.new(title: 'Olá mundo')
|
373
|
+
post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
|
374
|
+
post.save
|
375
|
+
end
|
376
|
+
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
377
|
+
# Title: Olá mundo
|
378
|
+
# Title: Olá mundo (from: example #5)
|
379
|
+
```
|
380
|
+
|
381
|
+
> **Note**: You can use `include ::Micro::Observers::For::ActiveModel` if your class only makes use of the `ActiveModel` and all the previous examples will work.
|
382
|
+
|
383
|
+
[⬆️ Back to Top](#table-of-contents-)
|
26
384
|
|
27
385
|
## Development
|
28
386
|
|
data/lib/micro/observers.rb
CHANGED
@@ -3,31 +3,11 @@ require 'micro/observers/version'
|
|
3
3
|
module Micro
|
4
4
|
module Observers
|
5
5
|
require 'micro/observers/utils'
|
6
|
-
require 'micro/observers/
|
7
|
-
require 'micro/observers/
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def notify_observers!(with:)
|
11
|
-
proc { |object| with.each { |evt_or_act| object.observers.notify(evt_or_act) } }
|
12
|
-
end
|
13
|
-
|
14
|
-
def notify_observers(*events)
|
15
|
-
notify_observers!(with: EventsOrActions[events])
|
16
|
-
end
|
17
|
-
|
18
|
-
def call_observers(options = Utils::EMPTY_HASH)
|
19
|
-
notify_observers!(with: EventsOrActions.fetch_actions(options))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.included(base)
|
24
|
-
base.extend(ClassMethods)
|
25
|
-
base.send(:private_class_method, :notify_observers!)
|
26
|
-
end
|
6
|
+
require 'micro/observers/event'
|
7
|
+
require 'micro/observers/set'
|
27
8
|
|
28
9
|
def observers
|
29
|
-
@
|
10
|
+
@__observers ||= Observers::Set.for(self)
|
30
11
|
end
|
31
|
-
|
32
12
|
end
|
33
13
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
|
6
|
+
class Event
|
7
|
+
require 'micro/observers/event/names'
|
8
|
+
|
9
|
+
attr_reader :name, :subject, :context, :data
|
10
|
+
|
11
|
+
def initialize(name, subject, context, data)
|
12
|
+
@name, @subject = name, subject
|
13
|
+
@context, @data = context, data
|
14
|
+
end
|
15
|
+
|
16
|
+
alias ctx context
|
17
|
+
alias subj subject
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
|
6
|
+
class Event::Names
|
7
|
+
def self.[](value, default: Utils::EMPTY_ARRAY)
|
8
|
+
values = Utils.compact_array(value)
|
9
|
+
|
10
|
+
values.empty? ? default : values
|
11
|
+
end
|
12
|
+
|
13
|
+
NO_EVENTS_MSG = 'no events (expected at least 1)'.freeze
|
14
|
+
|
15
|
+
def self.fetch(value)
|
16
|
+
values = self[value]
|
17
|
+
|
18
|
+
return values unless values.empty?
|
19
|
+
|
20
|
+
raise ArgumentError, NO_EVENTS_MSG
|
21
|
+
end
|
22
|
+
|
23
|
+
private_constant :NO_EVENTS_MSG
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
module For
|
6
|
+
|
7
|
+
module ActiveModel
|
8
|
+
module ClassMethods
|
9
|
+
def notify_observers!(events)
|
10
|
+
proc do |object|
|
11
|
+
object.observers.subject_changed!
|
12
|
+
object.observers.send(:broadcast_if_subject_changed, events)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def notify_observers(*events)
|
17
|
+
notify_observers!(Event::Names.fetch(events))
|
18
|
+
end
|
19
|
+
|
20
|
+
def notify_observers_on(*callback_methods)
|
21
|
+
Utils.compact_array(callback_methods).each do |callback_method|
|
22
|
+
self.public_send(callback_method, ¬ify_observers!([callback_method]))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.included(base)
|
28
|
+
base.extend(ClassMethods)
|
29
|
+
base.send(:private_class_method, :notify_observers!)
|
30
|
+
base.send(:include, ::Micro::Observers)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
module For
|
6
|
+
require 'micro/observers/for/active_model'
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
def self.included(base)
|
10
|
+
base.send(:include, ::Micro::Observers::For::ActiveModel)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
|
6
|
+
class Set
|
7
|
+
MapSubscriber = -> (observer, options) { [:observer, observer, options[:context]] }
|
8
|
+
|
9
|
+
MapSubscribers = -> (value) do
|
10
|
+
array = Utils.compact_array(value.kind_of?(Array) ? value : [])
|
11
|
+
array.map { |observer| MapSubscriber[observer, Utils::EMPTY_HASH] }
|
12
|
+
end
|
13
|
+
|
14
|
+
GetObserver = -> subscriber { subscriber[0] == :observer ? subscriber[1] : subscriber[2][0] }
|
15
|
+
|
16
|
+
EqualTo = -> (observer) { -> subscriber { GetObserver[subscriber] == observer } }
|
17
|
+
|
18
|
+
def self.for(subject)
|
19
|
+
new(subject)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(subject, subscribers: nil)
|
23
|
+
@subject = subject
|
24
|
+
|
25
|
+
@subject_changed = false
|
26
|
+
|
27
|
+
@subscribers = MapSubscribers.call(subscribers)
|
28
|
+
end
|
29
|
+
|
30
|
+
def count
|
31
|
+
@subscribers.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def none?
|
35
|
+
@subscribers.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def some?
|
39
|
+
!none?
|
40
|
+
end
|
41
|
+
|
42
|
+
def subject_changed?
|
43
|
+
@subject_changed
|
44
|
+
end
|
45
|
+
|
46
|
+
INVALID_BOOLEAN_MSG = 'expected a boolean (true, false)'.freeze
|
47
|
+
|
48
|
+
def subject_changed(state)
|
49
|
+
return @subject_changed = state if state == true || state == false
|
50
|
+
|
51
|
+
raise ArgumentError, INVALID_BOOLEAN_MSG
|
52
|
+
end
|
53
|
+
|
54
|
+
def subject_changed!
|
55
|
+
subject_changed(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def included?(observer)
|
59
|
+
@subscribers.any?(&EqualTo[observer])
|
60
|
+
end
|
61
|
+
|
62
|
+
def attach(*args)
|
63
|
+
options = args.last.is_a?(Hash) ? args.pop : Utils::EMPTY_HASH
|
64
|
+
|
65
|
+
Utils.compact_array(args).each do |observer|
|
66
|
+
@subscribers << MapSubscriber[observer, options] unless included?(observer)
|
67
|
+
end
|
68
|
+
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def detach(*args)
|
73
|
+
Utils.compact_array(args).each do |observer|
|
74
|
+
@subscribers.delete_if(&EqualTo[observer])
|
75
|
+
end
|
76
|
+
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def on(options = Utils::EMPTY_HASH)
|
81
|
+
event, callable, with = options[:event], options[:call], options[:with]
|
82
|
+
|
83
|
+
return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
|
84
|
+
|
85
|
+
@subscribers << [:callable, event, [callable, with]] unless included?(callable)
|
86
|
+
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def notify(*events, data: nil)
|
91
|
+
broadcast_if_subject_changed(Event::Names.fetch(events), data)
|
92
|
+
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
def notify!(*events, data: nil)
|
97
|
+
broadcast(Event::Names.fetch(events), data)
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
CALL_EVENT = [:call].freeze
|
103
|
+
|
104
|
+
def call(*events, data: nil)
|
105
|
+
broadcast_if_subject_changed(Event::Names[events, default: CALL_EVENT], data)
|
106
|
+
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
def call!(*events, data: nil)
|
111
|
+
broadcast(Event::Names[events, default: CALL_EVENT], data)
|
112
|
+
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def inspect
|
117
|
+
subs = @subscribers.empty? ? @subscribers : @subscribers.map(&GetObserver)
|
118
|
+
|
119
|
+
'<#%s @subject=%s @subject_changed=%p @subscribers=%p>' % [self.class, @subject, @subject_changed, subs]
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def broadcast_if_subject_changed(events, data = nil)
|
125
|
+
return unless subject_changed?
|
126
|
+
|
127
|
+
broadcast(events, data)
|
128
|
+
|
129
|
+
subject_changed(false)
|
130
|
+
end
|
131
|
+
|
132
|
+
def broadcast(event_names, data)
|
133
|
+
return if @subscribers.empty?
|
134
|
+
|
135
|
+
event_names.each do |event_name|
|
136
|
+
@subscribers.each do |strategy, observer, context|
|
137
|
+
case strategy
|
138
|
+
when :observer then notify_observer(observer, event_name, context, data)
|
139
|
+
when :callable then notify_callable(observer, event_name, context, data)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def notify_observer(observer, event_name, context, data)
|
146
|
+
return unless observer.respond_to?(event_name)
|
147
|
+
|
148
|
+
handler = observer.is_a?(Proc) ? observer : observer.method(event_name)
|
149
|
+
|
150
|
+
return handler.call(@subject) if handler.arity == 1
|
151
|
+
|
152
|
+
handler.call(@subject, Event.new(event_name, @subject, context, data))
|
153
|
+
end
|
154
|
+
|
155
|
+
def notify_callable(expected_event_name, event_name, context, data)
|
156
|
+
return if expected_event_name != event_name
|
157
|
+
|
158
|
+
callable, with = context[0], context[1]
|
159
|
+
callable_arg =
|
160
|
+
if with && !with.is_a?(Proc)
|
161
|
+
with
|
162
|
+
else
|
163
|
+
event = Event.new(event_name, @subject, nil, data)
|
164
|
+
|
165
|
+
with.is_a?(Proc) ? with.call(event) : event
|
166
|
+
end
|
167
|
+
|
168
|
+
callable.call(callable_arg)
|
169
|
+
end
|
170
|
+
|
171
|
+
private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT
|
172
|
+
private_constant :MapSubscriber, :MapSubscribers, :GetObserver, :EqualTo
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
data/test.sh
ADDED
data/u-observers.gemspec
CHANGED
@@ -10,7 +10,6 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = %q{Simple and powerful implementation of the observer pattern.}
|
11
11
|
spec.homepage = 'https://github.com/serradura/u-observers'
|
12
12
|
spec.license = 'MIT'
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.2.0')
|
14
13
|
|
15
14
|
spec.metadata['homepage_uri'] = spec.homepage
|
16
15
|
spec.metadata['source_code_uri'] = 'https://github.com/serradura/u-observers'
|
@@ -24,4 +23,9 @@ Gem::Specification.new do |spec|
|
|
24
23
|
spec.bindir = 'exe'
|
25
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
25
|
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.2.0')
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler'
|
30
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
27
31
|
end
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: u-observers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Serradura
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
12
|
-
dependencies:
|
11
|
+
date: 2020-10-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
13
41
|
description: Simple and powerful implementation of the observer pattern.
|
14
42
|
email:
|
15
43
|
- rodrigo.serradura@gmail.com
|
@@ -18,6 +46,8 @@ extensions: []
|
|
18
46
|
extra_rdoc_files: []
|
19
47
|
files:
|
20
48
|
- ".gitignore"
|
49
|
+
- ".tool-versions"
|
50
|
+
- ".travis.sh"
|
21
51
|
- ".travis.yml"
|
22
52
|
- CODE_OF_CONDUCT.md
|
23
53
|
- Gemfile
|
@@ -27,11 +57,17 @@ files:
|
|
27
57
|
- bin/console
|
28
58
|
- bin/setup
|
29
59
|
- lib/micro/observers.rb
|
30
|
-
- lib/micro/observers/
|
31
|
-
- lib/micro/observers/
|
60
|
+
- lib/micro/observers/event.rb
|
61
|
+
- lib/micro/observers/event/names.rb
|
62
|
+
- lib/micro/observers/for/active_model.rb
|
63
|
+
- lib/micro/observers/for/active_record.rb
|
64
|
+
- lib/micro/observers/set.rb
|
32
65
|
- lib/micro/observers/utils.rb
|
33
66
|
- lib/micro/observers/version.rb
|
34
67
|
- lib/u-observers.rb
|
68
|
+
- lib/u-observers/for/active_model.rb
|
69
|
+
- lib/u-observers/for/active_record.rb
|
70
|
+
- test.sh
|
35
71
|
- u-observers.gemspec
|
36
72
|
homepage: https://github.com/serradura/u-observers
|
37
73
|
licenses:
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Micro
|
4
|
-
module Observers
|
5
|
-
|
6
|
-
module EventsOrActions
|
7
|
-
DEFAULTS = [:call]
|
8
|
-
|
9
|
-
def self.[](value)
|
10
|
-
values = Utils.compact_array(value)
|
11
|
-
|
12
|
-
values.empty? ? DEFAULTS : values
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.fetch_actions(hash)
|
16
|
-
return self[hash.fetch(:actions) { hash.fetch(:action) }] if hash.is_a?(Hash)
|
17
|
-
|
18
|
-
raise ArgumentError, 'expected a hash with the key :action or :actions'
|
19
|
-
end
|
20
|
-
|
21
|
-
private_constant :DEFAULTS
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
25
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Micro
|
4
|
-
module Observers
|
5
|
-
|
6
|
-
class Manager
|
7
|
-
EqualTo = -> (observer) do
|
8
|
-
-> item { item[0] == :observer && item[1] == observer }
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.for(subject)
|
12
|
-
new(subject)
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(subject, list = nil)
|
16
|
-
@subject = subject
|
17
|
-
|
18
|
-
@list = Utils.compact_array(list.kind_of?(Array) ? list : [])
|
19
|
-
end
|
20
|
-
|
21
|
-
def included?(observer)
|
22
|
-
@list.any?(&EqualTo[observer])
|
23
|
-
end
|
24
|
-
|
25
|
-
def attach(observer, options = Utils::EMPTY_HASH)
|
26
|
-
if options[:allow_duplication] || !included?(observer)
|
27
|
-
@list << [:observer, observer, options[:data]]
|
28
|
-
end
|
29
|
-
|
30
|
-
self
|
31
|
-
end
|
32
|
-
|
33
|
-
def on(options = Utils::EMPTY_HASH)
|
34
|
-
event, callable, with = options[:event], options[:call], options[:with]
|
35
|
-
|
36
|
-
return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
|
37
|
-
|
38
|
-
arg = with.is_a?(Proc) ? with.call(@subject) : (arg || subject)
|
39
|
-
|
40
|
-
@list << [:callable, event, [callable, arg]]
|
41
|
-
end
|
42
|
-
|
43
|
-
def detach(observer)
|
44
|
-
@list.delete_if(&EqualTo[observer])
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
|
-
def notify(*events)
|
50
|
-
broadcast(EventsOrActions[events])
|
51
|
-
|
52
|
-
self
|
53
|
-
end
|
54
|
-
|
55
|
-
def call(options = Utils::EMPTY_HASH)
|
56
|
-
broadcast(EventsOrActions.fetch_actions(options))
|
57
|
-
|
58
|
-
self
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def broadcast(evts_or_acts)
|
64
|
-
evts_or_acts.each do |evt_or_act|
|
65
|
-
@list.each do |strategy, observer, data|
|
66
|
-
call!(observer, strategy, data, with: evt_or_act)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def call!(observer, strategy, data, with:)
|
72
|
-
return data[0].call(data[1]) if strategy == :callable && observer == with
|
73
|
-
|
74
|
-
if strategy == :observer && observer.respond_to?(with)
|
75
|
-
handler = observer.method(with)
|
76
|
-
|
77
|
-
return handler.call(@subject) if handler.arity == 1
|
78
|
-
|
79
|
-
handler.call(@subject, data)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
private_constant :EqualTo
|
84
|
-
end
|
85
|
-
|
86
|
-
end
|
87
|
-
end
|