u-observers 0.5.0 → 1.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 +271 -11
- data/lib/micro/observers.rb +2 -22
- data/lib/micro/observers/events.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/manager.rb +114 -27
- 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 +39 -4
- data/lib/micro/observers/events_or_actions.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c90f3338948cf8db02bdb1a882e3191adfd52f106fc2b4c1acf873447abd82a
|
4
|
+
data.tar.gz: 2db659fbaee101e1af2838bdb89dc85838f0e9686d14fe14fd9782fc568ee1f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 674b8dabdc6cfca0a982c1f72582bf1fcaab34ead0f0de29d4975cc62e164c13bbded5e6118152951584fb5855b6c539510ca393b36f9cf56622bf663d7b22e2
|
7
|
+
data.tar.gz: ff1e33f5029a40745172c7196d7e21552e8f4d534c71f057bf5643e3262fe8fa243fb55219eed352195a09abc51c337476f9f8bfcd5bce5f61cee416c2ec8de7
|
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,288 @@
|
|
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
|
+
- [Calling the observers](#calling-the-observers)
|
39
|
+
- [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
|
40
|
+
- [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
|
41
|
+
- [notify_observers_on()](#notify_observers_on)
|
42
|
+
- [notify_observers()](#notify_observers)
|
43
|
+
- [Development](#development)
|
44
|
+
- [Contributing](#contributing)
|
45
|
+
- [License](#license)
|
46
|
+
- [Code of Conduct](#code-of-conduct)
|
47
|
+
|
48
|
+
# Installation
|
49
|
+
|
50
|
+
Add this line to your application's Gemfile and `bundle install`:
|
10
51
|
|
11
52
|
```ruby
|
12
|
-
gem '
|
53
|
+
gem 'u-observers'
|
13
54
|
```
|
14
55
|
|
15
|
-
|
56
|
+
# Compatibility
|
16
57
|
|
17
|
-
|
58
|
+
| u-observers | branch | ruby | activerecord |
|
59
|
+
| ----------- | ------- | -------- | ------------- |
|
60
|
+
| 1.0.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
|
18
61
|
|
19
|
-
|
62
|
+
> **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
63
|
|
21
|
-
|
64
|
+
[⬆️ Back to Top](#table-of-contents-)
|
22
65
|
|
23
66
|
## Usage
|
24
67
|
|
25
|
-
|
68
|
+
Any class with `Micro::Observers` module included can notify events to attached observers.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
require 'securerandom'
|
72
|
+
|
73
|
+
class Order
|
74
|
+
include Micro::Observers
|
75
|
+
|
76
|
+
attr_reader :code
|
77
|
+
|
78
|
+
def initialize
|
79
|
+
@code, @status = SecureRandom.alphanumeric, :draft
|
80
|
+
end
|
81
|
+
|
82
|
+
def canceled?
|
83
|
+
@status == :canceled
|
84
|
+
end
|
85
|
+
|
86
|
+
def cancel!
|
87
|
+
return self if canceled?
|
88
|
+
|
89
|
+
@status = :canceled
|
90
|
+
|
91
|
+
observers.subject_changed!
|
92
|
+
observers.notify(:canceled) and return self
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module OrderEvents
|
97
|
+
def self.canceled(order)
|
98
|
+
puts "The order #(#{order.code}) has been canceled."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
order = Order.new
|
103
|
+
#<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
|
104
|
+
|
105
|
+
order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
|
106
|
+
# <#Micro::Observers::Manager @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]
|
107
|
+
|
108
|
+
order.canceled?
|
109
|
+
# false
|
110
|
+
|
111
|
+
order.cancel!
|
112
|
+
# The message below will be printed by the observer (OrderEvents):
|
113
|
+
# The order #(X0o9yf1GsdQFvLR4) has been canceled
|
114
|
+
|
115
|
+
order.canceled?
|
116
|
+
# true
|
117
|
+
```
|
118
|
+
|
119
|
+
**Highlights of the previous example:**
|
120
|
+
|
121
|
+
To avoid an undesired behavior, do you need to mark the subject as changed before notify your observers about some event.
|
122
|
+
|
123
|
+
You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
|
124
|
+
|
125
|
+
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)`
|
126
|
+
|
127
|
+
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.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
order.observers.notify
|
131
|
+
# ArgumentError (no events (expected at least 1))
|
132
|
+
```
|
133
|
+
|
134
|
+
[⬆️ Back to Top](#table-of-contents-)
|
135
|
+
|
136
|
+
### Passing a context for your observers
|
137
|
+
|
138
|
+
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.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class Order
|
142
|
+
include Micro::Observers
|
143
|
+
|
144
|
+
def cancel!
|
145
|
+
observers.subject_changed!
|
146
|
+
observers.notify(:canceled)
|
147
|
+
self
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module OrderEvents
|
152
|
+
def self.canceled(order, context)
|
153
|
+
puts "The order #(#{order.code}) has been canceled. (from: #{context[:from]})"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
order = Order.new
|
158
|
+
order.observers.attach(OrderEvents, context: { from: 'example #2' ) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
|
159
|
+
order.cancel!
|
160
|
+
# The message below will be printed by the observer (OrderEvents):
|
161
|
+
# The order #(70196221441820) has been canceled. (from: example #2)
|
162
|
+
```
|
163
|
+
|
164
|
+
[⬆️ Back to Top](#table-of-contents-)
|
165
|
+
|
166
|
+
### Calling the observers
|
167
|
+
|
168
|
+
You can use a callable (a class, module, or object that responds to the call method) to be your observers.
|
169
|
+
To do this, you only need make use of the method `#call` instead of `#notify`.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class Order
|
173
|
+
include Micro::Observers
|
174
|
+
|
175
|
+
def cancel!
|
176
|
+
observers.subject_changed!
|
177
|
+
observers.call # in practice, this is a shortcut to observers.notify(:call)
|
178
|
+
self
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
|
183
|
+
|
184
|
+
order = Order.new
|
185
|
+
order.observers.attach(OrderCancellation)
|
186
|
+
order.cancel!
|
187
|
+
# The message below will be printed by the observer (OrderEvents):
|
188
|
+
# The order #(70196221441820) has been canceled.
|
189
|
+
```
|
190
|
+
|
191
|
+
PS: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.a
|
192
|
+
|
193
|
+
[⬆️ Back to Top](#table-of-contents-)
|
194
|
+
|
195
|
+
### Notifying observers without marking them as changed
|
196
|
+
|
197
|
+
This feature needs to be used with caution!
|
198
|
+
|
199
|
+
If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
|
200
|
+
|
201
|
+
[⬆️ Back to Top](#table-of-contents-)
|
202
|
+
|
203
|
+
### ActiveRecord and ActiveModel integrations
|
204
|
+
|
205
|
+
To make use of this feature you need to require an additional module (`require 'u-observers/for/active_record'`).
|
206
|
+
|
207
|
+
Gemfile example:
|
208
|
+
```ruby
|
209
|
+
gem 'u-observers', require: 'u-observers/for/active_record'
|
210
|
+
```
|
211
|
+
|
212
|
+
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:
|
213
|
+
|
214
|
+
|
215
|
+
#### notify_observers_on()
|
216
|
+
|
217
|
+
The `notify_observers_on` allows you to pass one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class Post < ActiveRecord::Base
|
221
|
+
include ::Micro::Observers::For::ActiveRecord
|
222
|
+
|
223
|
+
notify_observers_on(:after_commit) # passing multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
|
224
|
+
end
|
225
|
+
|
226
|
+
module TitlePrinter
|
227
|
+
def self.after_commit(post)
|
228
|
+
puts "Title: #{post.title}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
module TitlePrinterWithContext
|
233
|
+
def self.after_commit(post, context)
|
234
|
+
puts "Title: #{post.title} (from: #{context[:from]})"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
Post.transaction do
|
239
|
+
post = Post.new(title: 'Hello world')
|
240
|
+
post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example 4' })
|
241
|
+
post.save
|
242
|
+
end
|
243
|
+
# The message below will be printed by the observer (OrderEvents):
|
244
|
+
# Title: Hello world
|
245
|
+
# Title: Hello world (from: example 4)
|
246
|
+
```
|
247
|
+
|
248
|
+
[⬆️ Back to Top](#table-of-contents-)
|
249
|
+
|
250
|
+
#### notify_observers()
|
251
|
+
|
252
|
+
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.
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class Post < ActiveRecord::Base
|
256
|
+
include ::Micro::Observers::For::ActiveRecord
|
257
|
+
|
258
|
+
after_commit(¬ify_observers(:transaction_completed))
|
259
|
+
end
|
260
|
+
|
261
|
+
module TitlePrinter
|
262
|
+
def self.transaction_completed(post)
|
263
|
+
puts("Title: #{post.title}")
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
module TitlePrinterWithContext
|
268
|
+
def self.transaction_completed(post, context)
|
269
|
+
puts("Title: #{post.title} (from: #{context[:from]})")
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
Post.transaction do
|
274
|
+
post = Post.new(title: 'Olá mundo')
|
275
|
+
post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example 5' })
|
276
|
+
post.save
|
277
|
+
end
|
278
|
+
# The message below will be printed by the observer (OrderEvents):
|
279
|
+
# Title: Olá mundo
|
280
|
+
# Title: Olá mundo (from: example 5)
|
281
|
+
```
|
282
|
+
|
283
|
+
PS: You can use `include ::Micro::Observers::For::ActiveModel` if your class only makes use of the `ActiveModel` and all the previous examples will work.
|
284
|
+
|
285
|
+
[⬆️ Back to Top](#table-of-contents-)
|
26
286
|
|
27
287
|
## Development
|
28
288
|
|
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/
|
6
|
+
require 'micro/observers/events'
|
7
7
|
require 'micro/observers/manager'
|
8
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
|
27
|
-
|
28
9
|
def observers
|
29
|
-
@
|
10
|
+
@__observers ||= Observers::Manager.for(self)
|
30
11
|
end
|
31
|
-
|
32
12
|
end
|
33
13
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Observers
|
5
|
+
|
6
|
+
module Events
|
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!(Events.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
|
@@ -4,27 +4,78 @@ module Micro
|
|
4
4
|
module Observers
|
5
5
|
|
6
6
|
class Manager
|
7
|
-
|
8
|
-
|
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] }
|
9
12
|
end
|
10
13
|
|
14
|
+
GetObserver = -> subscriber { subscriber[0] == :observer ? subscriber[1] : subscriber[2][0] }
|
15
|
+
|
16
|
+
EqualTo = -> (observer) { -> subscriber { GetObserver[subscriber] == observer } }
|
17
|
+
|
11
18
|
def self.for(subject)
|
12
19
|
new(subject)
|
13
20
|
end
|
14
21
|
|
15
|
-
def initialize(subject,
|
22
|
+
def initialize(subject, subscribers: nil)
|
16
23
|
@subject = subject
|
17
24
|
|
18
|
-
@
|
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
|
+
if state == true || state == false
|
50
|
+
@subject_changed = state
|
51
|
+
|
52
|
+
return self
|
53
|
+
end
|
54
|
+
|
55
|
+
raise ArgumentError, INVALID_BOOLEAN_MSG
|
56
|
+
end
|
57
|
+
|
58
|
+
def subject_changed!
|
59
|
+
subject_changed(true)
|
19
60
|
end
|
20
61
|
|
21
62
|
def included?(observer)
|
22
|
-
@
|
63
|
+
@subscribers.any?(&EqualTo[observer])
|
23
64
|
end
|
24
65
|
|
25
|
-
def attach(
|
26
|
-
|
27
|
-
|
66
|
+
def attach(*args)
|
67
|
+
options = args.last.is_a?(Hash) ? args.pop : Utils::EMPTY_HASH
|
68
|
+
|
69
|
+
Utils.compact_array(args).each do |observer|
|
70
|
+
@subscribers << MapSubscriber[observer, options] unless included?(observer)
|
71
|
+
end
|
72
|
+
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def detach(*args)
|
77
|
+
Utils.compact_array(args).each do |observer|
|
78
|
+
@subscribers.delete_if(&EqualTo[observer])
|
28
79
|
end
|
29
80
|
|
30
81
|
self
|
@@ -35,50 +86,86 @@ module Micro
|
|
35
86
|
|
36
87
|
return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
|
37
88
|
|
38
|
-
|
89
|
+
@subscribers << [:callable, event, [callable, with]] unless included?(callable)
|
39
90
|
|
40
|
-
|
91
|
+
self
|
41
92
|
end
|
42
93
|
|
43
|
-
def
|
44
|
-
|
94
|
+
def notify(*events)
|
95
|
+
broadcast_if_subject_changed(Events.fetch(events))
|
45
96
|
|
46
97
|
self
|
47
98
|
end
|
48
99
|
|
49
|
-
def notify(*events)
|
50
|
-
|
100
|
+
def notify!(*events)
|
101
|
+
broadcast(Events.fetch(events))
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
CALL_EVENT = [:call].freeze
|
107
|
+
|
108
|
+
def call(*events)
|
109
|
+
broadcast_if_subject_changed(Events[events, default: CALL_EVENT])
|
51
110
|
|
52
111
|
self
|
53
112
|
end
|
54
113
|
|
55
|
-
def call(
|
56
|
-
|
114
|
+
def call!(*events)
|
115
|
+
broadcast(Events[events, default: CALL_EVENT])
|
57
116
|
|
58
117
|
self
|
59
118
|
end
|
60
119
|
|
120
|
+
def inspect
|
121
|
+
subs = @subscribers.empty? ? @subscribers : @subscribers.map(&GetObserver)
|
122
|
+
|
123
|
+
'<#%s @subject=%s @subject_changed=%p @subscribers=%p>' % [self.class, @subject, @subject_changed, subs]
|
124
|
+
end
|
125
|
+
|
61
126
|
private
|
62
127
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
128
|
+
def broadcast_if_subject_changed(events)
|
129
|
+
return unless subject_changed?
|
130
|
+
|
131
|
+
broadcast(events)
|
132
|
+
|
133
|
+
subject_changed(false)
|
134
|
+
end
|
135
|
+
|
136
|
+
def broadcast(events)
|
137
|
+
return if @subscribers.empty?
|
138
|
+
|
139
|
+
events.each do |event|
|
140
|
+
@subscribers.each do |strategy, observer, context|
|
141
|
+
case strategy
|
142
|
+
when :observer then notify_observer(observer, event, context)
|
143
|
+
when :callable then notify_callable(observer, event, context)
|
144
|
+
end
|
145
|
+
end
|
66
146
|
end
|
67
147
|
end
|
68
148
|
|
69
|
-
def
|
70
|
-
return
|
149
|
+
def notify_observer(observer, method_name, context)
|
150
|
+
return unless observer.respond_to?(method_name)
|
71
151
|
|
72
|
-
|
73
|
-
handler = observer.method(with)
|
152
|
+
handler = observer.is_a?(Proc) ? observer : observer.method(method_name)
|
74
153
|
|
75
|
-
|
154
|
+
handler.arity == 1 ? handler.call(@subject) : handler.call(@subject, context)
|
155
|
+
end
|
76
156
|
|
77
|
-
|
78
|
-
|
157
|
+
def notify_callable(expected_event, event, context)
|
158
|
+
return if expected_event != event
|
159
|
+
|
160
|
+
callable, with = context[0], context[1]
|
161
|
+
|
162
|
+
arg = with.is_a?(Proc) ? with.call(@subject) : (with || @subject)
|
163
|
+
|
164
|
+
callable.call(arg)
|
79
165
|
end
|
80
166
|
|
81
|
-
private_constant :
|
167
|
+
private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT
|
168
|
+
private_constant :MapSubscriber, :MapSubscribers, :GetObserver, :EqualTo
|
82
169
|
end
|
83
170
|
|
84
171
|
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: 1.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-05 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,16 @@ files:
|
|
27
57
|
- bin/console
|
28
58
|
- bin/setup
|
29
59
|
- lib/micro/observers.rb
|
30
|
-
- lib/micro/observers/
|
60
|
+
- lib/micro/observers/events.rb
|
61
|
+
- lib/micro/observers/for/active_model.rb
|
62
|
+
- lib/micro/observers/for/active_record.rb
|
31
63
|
- lib/micro/observers/manager.rb
|
32
64
|
- lib/micro/observers/utils.rb
|
33
65
|
- lib/micro/observers/version.rb
|
34
66
|
- lib/u-observers.rb
|
67
|
+
- lib/u-observers/for/active_model.rb
|
68
|
+
- lib/u-observers/for/active_record.rb
|
69
|
+
- test.sh
|
35
70
|
- u-observers.gemspec
|
36
71
|
homepage: https://github.com/serradura/u-observers
|
37
72
|
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
|