u-observers 2.3.0 → 3.0.0
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 +4 -4
- data/.github/workflows/ci.yml +66 -0
- data/.gitignore +8 -0
- data/.tool-versions +1 -1
- data/Appraisals +92 -0
- data/CHANGELOG.md +169 -0
- data/CLAUDE.md +157 -0
- data/Gemfile +6 -32
- data/README.md +115 -46
- data/README.pt-BR.md +115 -45
- data/Rakefile +31 -1
- data/bin/matrix +16 -0
- data/bin/setup +4 -0
- data/lib/micro/observers/for/active_model.rb +118 -4
- data/lib/micro/observers/version.rb +1 -1
- data/u-observers.gemspec +6 -4
- metadata +28 -14
- data/.travis.sh +0 -34
- data/.travis.yml +0 -30
- data/test.sh +0 -11
data/README.md
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">👀 μ-observers</h1>
|
|
3
3
|
<p align="center"><i>Simple and powerful implementation of the observer pattern.</i></p>
|
|
4
|
-
<br>
|
|
5
4
|
</p>
|
|
6
5
|
|
|
7
6
|
<p align="center">
|
|
8
|
-
<img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
|
9
|
-
|
|
10
7
|
<a href="https://rubygems.org/gems/u-observers">
|
|
11
8
|
<img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
|
|
12
9
|
</a>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<img alt="Build Status" src="https://travis-ci.com/serradura/u-observers.svg?branch=main">
|
|
16
|
-
</a>
|
|
17
|
-
|
|
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">
|
|
10
|
+
<a href="https://github.com/u-gems/u-observers/actions/workflows/ci.yml">
|
|
11
|
+
<img alt="Build Status" src="https://github.com/u-gems/u-observers/actions/workflows/ci.yml/badge.svg">
|
|
24
12
|
</a>
|
|
13
|
+
<br/>
|
|
14
|
+
<a href="https://qlty.sh/gh/u-gems/projects/u-observers"><img src="https://qlty.sh/gh/u-gems/projects/u-observers/maintainability.svg" alt="Maintainability" /></a>
|
|
15
|
+
<a href="https://qlty.sh/gh/u-gems/projects/u-observers"><img src="https://qlty.sh/gh/u-gems/projects/u-observers/coverage.svg" alt="Code Coverage" /></a>
|
|
16
|
+
<br/>
|
|
17
|
+
<img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
|
|
18
|
+
<img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
|
|
25
19
|
</p>
|
|
26
20
|
|
|
21
|
+
> [!IMPORTANT]
|
|
22
|
+
> **No breaking API changes — ever.** `u-observers` is a dependency-free gem that many projects rely on (directly or transitively). Its role is to remain a stable, backward-compatible foundation — every change keeps existing code working.
|
|
23
|
+
>
|
|
24
|
+
> Major version bumps signal only that a Ruby or Rails version was dropped from the supported matrix — per SemVer, a dependency-floor change. Your code keeps working.
|
|
25
|
+
|
|
27
26
|
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
27
|
|
|
29
28
|
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 compromised.
|
|
30
29
|
|
|
31
30
|
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/object).
|
|
32
31
|
|
|
33
|
-
> **Note:** Você entende português? 🇧🇷 🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/
|
|
32
|
+
> **Note:** Você entende português? 🇧🇷 🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/u-gems/u-observers/blob/main/README.pt-BR.md).
|
|
34
33
|
|
|
35
34
|
# Table of contents <!-- omit in toc -->
|
|
35
|
+
|
|
36
36
|
- [Installation](#installation)
|
|
37
37
|
- [Compatibility](#compatibility)
|
|
38
38
|
- [Usage](#usage)
|
|
@@ -46,13 +46,14 @@ Because of this issue, I decided to create a gem that encapsulates the pattern w
|
|
|
46
46
|
- [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
|
|
47
47
|
- [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
|
|
48
48
|
- [Defining observers using blocks](#defining-observers-using-blocks)
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
49
|
+
- [`observers.on()`](#observerson)
|
|
50
|
+
- [`observers.once()`](#observersonce)
|
|
51
51
|
- [Replacing a block by a `lambda`/`proc`](#replacing-a-block-by-a-lambdaproc)
|
|
52
52
|
- [Detaching observers](#detaching-observers)
|
|
53
53
|
- [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
|
|
54
|
-
- [notify_observers_on()](#notify_observers_on)
|
|
55
|
-
- [notify_observers()](#notify_observers)
|
|
54
|
+
- [`.notify_observers_on()`](#notify_observers_on)
|
|
55
|
+
- [Attaching observers at the class level (`.notify_observers!()`)](#attaching-observers-at-the-class-level-notify_observers)
|
|
56
|
+
- [`.notify_observers()`](#notify_observers)
|
|
56
57
|
- [Development](#development)
|
|
57
58
|
- [Contributing](#contributing)
|
|
58
59
|
- [License](#license)
|
|
@@ -68,11 +69,24 @@ gem 'u-observers'
|
|
|
68
69
|
|
|
69
70
|
# Compatibility
|
|
70
71
|
|
|
71
|
-
| u-observers | branch
|
|
72
|
-
| ----------- |
|
|
73
|
-
|
|
|
74
|
-
| 2.3.0 | v2.x
|
|
75
|
-
| 1.0.0 | v1.x
|
|
72
|
+
| u-observers | branch | ruby | activerecord |
|
|
73
|
+
| ----------- | ------ | -------- | --------------- |
|
|
74
|
+
| 3.0.0 | main | >= 2.7.0 | >= 6.0, <= Edge |
|
|
75
|
+
| 2.3.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
|
76
|
+
| 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
|
77
|
+
|
|
78
|
+
This library is tested (CI matrix) against:
|
|
79
|
+
|
|
80
|
+
| Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
|
|
81
|
+
| ------------ | --- | --- | --- | --- | --- | --- | --- | ---- |
|
|
82
|
+
| 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
83
|
+
| 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
84
|
+
| 3.1 | | | ✅ | ✅ | ✅ | | | |
|
|
85
|
+
| 3.2 | | | ✅ | ✅ | ✅ | ✅ | | |
|
|
86
|
+
| 3.3 | | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
87
|
+
| 3.4 | | | | | ✅ | ✅ | ✅ | ✅ |
|
|
88
|
+
| 4.x | | | | | | | ✅ | ✅ |
|
|
89
|
+
| Head | | | | | | | ✅ | ✅ |
|
|
76
90
|
|
|
77
91
|
> **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).
|
|
78
92
|
|
|
@@ -219,8 +233,8 @@ order.observers.notify(:changed, data: 1)
|
|
|
219
233
|
The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
|
|
220
234
|
|
|
221
235
|
- `#name` will be the broadcasted event.
|
|
222
|
-
- `#subject` will be the observed
|
|
223
|
-
- `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined
|
|
236
|
+
- `#subject` will be the observed object.
|
|
237
|
+
- `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined at the moment that you attach the observer.
|
|
224
238
|
- `#data` will be [the value that was shared in the observers' notification](#sharing-data-when-notifying-the-observers).
|
|
225
239
|
- `#ctx` is an alias for the `#context` method.
|
|
226
240
|
- `#subj` is an alias for the `#subject` method.
|
|
@@ -231,9 +245,10 @@ The `Micro::Observers::Event` is the event payload. Follow below all of its prop
|
|
|
231
245
|
|
|
232
246
|
The `observers.on()` method enables you to attach a callable as an observer.
|
|
233
247
|
|
|
234
|
-
Usually, a callable has a well-defined responsibility (do only one thing), because of this, it tends to be more [SRP (Single-responsibility principle)](https://en.wikipedia.org/wiki/Single-responsibility_principle)
|
|
248
|
+
Usually, a callable has a well-defined responsibility (do only one thing), because of this, it tends to be more [SRP (Single-responsibility principle)](https://en.wikipedia.org/wiki/Single-responsibility_principle) friendly than a conventional observer (that could have N methods to respond to different kinds of notification).
|
|
235
249
|
|
|
236
250
|
This method receives the below options:
|
|
251
|
+
|
|
237
252
|
1. `:event` the expected event name.
|
|
238
253
|
2. `:call` the callable object itself.
|
|
239
254
|
3. `:with` (optional) it can define the value which will be used as the callable object's argument. So, if it is a `Proc`, a `Micro::Observers::Event` instance will be received as the `Proc` argument, and its output will be the callable argument. But if this option wasn't defined, the `Micro::Observers::Event` instance will be the callable argument.
|
|
@@ -381,9 +396,9 @@ order.cancel! # Nothing will happen because there aren't observers.
|
|
|
381
396
|
|
|
382
397
|
### Defining observers using blocks
|
|
383
398
|
|
|
384
|
-
The methods `#on()` and `#once()` can receive
|
|
399
|
+
The methods `#on()` and `#once()` can receive an event (`symbol`) and a block to define observers.
|
|
385
400
|
|
|
386
|
-
####
|
|
401
|
+
#### `observers.on()`
|
|
387
402
|
|
|
388
403
|
```ruby
|
|
389
404
|
class Order
|
|
@@ -406,7 +421,7 @@ order.cancel! # The order #(70301497466060) has been canceled.
|
|
|
406
421
|
order.observers.some? # true
|
|
407
422
|
```
|
|
408
423
|
|
|
409
|
-
####
|
|
424
|
+
#### `observers.once()`
|
|
410
425
|
|
|
411
426
|
```ruby
|
|
412
427
|
class Order
|
|
@@ -431,7 +446,7 @@ order.observers.some? # false
|
|
|
431
446
|
|
|
432
447
|
#### Replacing a block by a `lambda`/`proc`
|
|
433
448
|
|
|
434
|
-
Ruby allows you to replace any block with a `lambda`/`proc`. e.g.
|
|
449
|
+
Ruby allows you to replace any block with a `lambda`/`proc`. So, it will be possible to use this kind of feature to define your observers. e.g.
|
|
435
450
|
|
|
436
451
|
```ruby
|
|
437
452
|
class Order
|
|
@@ -475,11 +490,12 @@ module OrderNotifications
|
|
|
475
490
|
end
|
|
476
491
|
|
|
477
492
|
order = Order.new
|
|
493
|
+
order.observers.on(:canceled) { |_event| }
|
|
478
494
|
order.observers.on(event: :canceled, call: NotifyAfterCancel)
|
|
479
495
|
order.observers.attach(OrderNotifications)
|
|
480
496
|
|
|
481
497
|
order.observers.some? # true
|
|
482
|
-
order.observers.count #
|
|
498
|
+
order.observers.count # 3
|
|
483
499
|
|
|
484
500
|
order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
|
|
485
501
|
order.observers.some? # true
|
|
@@ -497,15 +513,16 @@ order.observers.count # 0
|
|
|
497
513
|
To make use of this feature you need to require an additional module.
|
|
498
514
|
|
|
499
515
|
Gemfile example:
|
|
516
|
+
|
|
500
517
|
```ruby
|
|
501
518
|
gem 'u-observers', require: 'u-observers/for/active_record'
|
|
502
519
|
```
|
|
503
520
|
|
|
504
521
|
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:
|
|
505
522
|
|
|
506
|
-
#### notify_observers_on()
|
|
523
|
+
#### `.notify_observers_on()`
|
|
507
524
|
|
|
508
|
-
The `notify_observers_on` allows you to define one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your
|
|
525
|
+
The `notify_observers_on` allows you to define one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your observers.
|
|
509
526
|
|
|
510
527
|
```ruby
|
|
511
528
|
class Post < ActiveRecord::Base
|
|
@@ -545,9 +562,64 @@ end
|
|
|
545
562
|
|
|
546
563
|
[⬆️ Back to Top](#table-of-contents-)
|
|
547
564
|
|
|
548
|
-
#### notify_observers()
|
|
565
|
+
#### Attaching observers at the class level (`.notify_observers!()`)
|
|
566
|
+
|
|
567
|
+
While `notify_observers_on` only wires the callback to a broadcast (you still `attach` the observers on every instance), `notify_observers!` also **binds the observers to the model at the class level** through the required `with:` option — so you never call `observers.attach` yourself. The `event:` option names the callback to hook; use `context:` to forward a context to those observers, and pass any extra option (e.g. `on:`) straight through to the underlying callback.
|
|
568
|
+
|
|
569
|
+
```ruby
|
|
570
|
+
class Post < ActiveRecord::Base
|
|
571
|
+
include ::Micro::Observers::For::ActiveRecord
|
|
572
|
+
|
|
573
|
+
# Attach TitlePrinter (and TitlePrinterWithContext) on every after_commit
|
|
574
|
+
# triggered by an update — no per-instance `observers.attach` needed.
|
|
575
|
+
notify_observers!(
|
|
576
|
+
on: :update,
|
|
577
|
+
with: [TitlePrinter, TitlePrinterWithContext],
|
|
578
|
+
event: :after_commit,
|
|
579
|
+
context: { from: 'class-level' }
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Equivalent to:
|
|
583
|
+
#
|
|
584
|
+
# after_commit(on: :update) do |record|
|
|
585
|
+
# record.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'class-level' })
|
|
586
|
+
# record.observers.subject_changed!
|
|
587
|
+
# record.observers.notify(:after_commit)
|
|
588
|
+
# end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
Post.transaction { Post.create(title: 'Hello world') } # nothing — `on: :update`
|
|
592
|
+
|
|
593
|
+
post = Post.first
|
|
594
|
+
Post.transaction { post.update(title: 'Hello again') }
|
|
595
|
+
# Title: Hello again
|
|
596
|
+
# Title: Hello again (from: class-level)
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
> **Note**: `event:` and `with:` are required (`with:` accepts a single observer or an array). Without observers to attach, use `notify_observers_on` instead.
|
|
600
|
+
|
|
601
|
+
The declared observers are introspectable and detachable at the class level:
|
|
602
|
+
|
|
603
|
+
```ruby
|
|
604
|
+
Post.observers_to_notify
|
|
605
|
+
# { after_commit: [TitlePrinter, TitlePrinterWithContext] }
|
|
606
|
+
|
|
607
|
+
# Stop notifying a given observer (from every callback, or scope it with `from:`)
|
|
608
|
+
Post.detach_observers_to_notify(TitlePrinterWithContext)
|
|
609
|
+
# { after_commit: [TitlePrinter] }
|
|
610
|
+
|
|
611
|
+
Post.detach_observers_to_notify(TitlePrinter, from: :after_commit)
|
|
612
|
+
# {}
|
|
613
|
+
|
|
614
|
+
# With no observers, clears the callback(s) entirely:
|
|
615
|
+
Post.detach_observers_to_notify(from: :after_commit)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
619
|
+
|
|
620
|
+
#### `.notify_observers()`
|
|
549
621
|
|
|
550
|
-
The `notify_observers` allows you to define one or more
|
|
622
|
+
The `notify_observers` allows you to define one or more _events_, that will be used to notify after the execution of some `ActiveModel`/`ActiveRecord` callback.
|
|
551
623
|
|
|
552
624
|
```ruby
|
|
553
625
|
class Post < ActiveRecord::Base
|
|
@@ -563,12 +635,6 @@ class Post < ActiveRecord::Base
|
|
|
563
635
|
# end
|
|
564
636
|
end
|
|
565
637
|
|
|
566
|
-
module TitlePrinter
|
|
567
|
-
def self.transaction_completed(post)
|
|
568
|
-
puts("Title: #{post.title}")
|
|
569
|
-
end
|
|
570
|
-
end
|
|
571
|
-
|
|
572
638
|
module TitlePrinterWithContext
|
|
573
639
|
def self.transaction_completed(post, event)
|
|
574
640
|
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
|
@@ -577,7 +643,11 @@ end
|
|
|
577
643
|
|
|
578
644
|
Post.transaction do
|
|
579
645
|
post = Post.new(title: 'Olá mundo')
|
|
580
|
-
|
|
646
|
+
|
|
647
|
+
post.observers.on(:transaction_completed) { |event| puts("Title: #{event.subject.title}") }
|
|
648
|
+
|
|
649
|
+
post.observers.attach(TitlePrinterWithContext, context: { from: 'example #7' })
|
|
650
|
+
|
|
581
651
|
post.save
|
|
582
652
|
end
|
|
583
653
|
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
|
@@ -597,8 +667,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
597
667
|
|
|
598
668
|
## Contributing
|
|
599
669
|
|
|
600
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
601
|
-
|
|
670
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/u-gems/u-observers. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/u-gems/u-observers/blob/master/CODE_OF_CONDUCT.md).
|
|
602
671
|
|
|
603
672
|
## License
|
|
604
673
|
|
|
@@ -606,4 +675,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
606
675
|
|
|
607
676
|
## Code of Conduct
|
|
608
677
|
|
|
609
|
-
Everyone interacting in the `Micro::Observers` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
678
|
+
Everyone interacting in the `Micro::Observers` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/u-gems/u-observers/blob/master/CODE_OF_CONDUCT.md).
|
data/README.pt-BR.md
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">👀 μ-observers</h1>
|
|
3
3
|
<p align="center"><i>Implementação simples e poderosa do padrão observer.</i></p>
|
|
4
|
-
<br>
|
|
5
4
|
</p>
|
|
6
5
|
|
|
7
6
|
<p align="center">
|
|
8
|
-
<img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
|
9
|
-
|
|
10
7
|
<a href="https://rubygems.org/gems/u-observers">
|
|
11
8
|
<img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
|
|
12
9
|
</a>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<img alt="Build Status" src="https://travis-ci.com/serradura/u-observers.svg?branch=main">
|
|
16
|
-
</a>
|
|
17
|
-
|
|
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">
|
|
10
|
+
<a href="https://github.com/u-gems/u-observers/actions/workflows/ci.yml">
|
|
11
|
+
<img alt="Build Status" src="https://github.com/u-gems/u-observers/actions/workflows/ci.yml/badge.svg">
|
|
24
12
|
</a>
|
|
13
|
+
<br/>
|
|
14
|
+
<a href="https://qlty.sh/gh/u-gems/projects/u-observers"><img src="https://qlty.sh/gh/u-gems/projects/u-observers/maintainability.svg" alt="Maintainability" /></a>
|
|
15
|
+
<a href="https://qlty.sh/gh/u-gems/projects/u-observers"><img src="https://qlty.sh/gh/u-gems/projects/u-observers/coverage.svg" alt="Code Coverage" /></a>
|
|
16
|
+
<br/>
|
|
17
|
+
<img src="https://img.shields.io/badge/Ruby%20%3E%3D%202.7%2C%20%3C%3D%20Head-ruby.svg?colorA=444&colorB=333" alt="Ruby">
|
|
18
|
+
<img src="https://img.shields.io/badge/Rails%20%3E%3D%206.0%2C%20%3C%3D%20Edge-rails.svg?colorA=444&colorB=333" alt="Rails">
|
|
25
19
|
</p>
|
|
26
20
|
|
|
21
|
+
> [!IMPORTANT]
|
|
22
|
+
> **Nenhuma mudança que quebre a API — nunca.** `u-observers` é uma gem sem dependências da qual muitos projetos dependem (direta ou transitivamente). Seu papel é permanecer uma base estável e retrocompatível — toda mudança mantém o código existente funcionando.
|
|
23
|
+
>
|
|
24
|
+
> Saltos de versão major sinalizam apenas que uma versão de Ruby ou Rails foi removida da matriz suportada — pela SemVer, uma mudança no piso de dependências. Seu código continua funcionando.
|
|
25
|
+
|
|
27
26
|
Esta gem implementa o padrão observer[[1]](https://en.wikipedia.org/wiki/Observer_pattern)[[2]](https://refactoring.guru/design-patterns/observer) (também conhecido como publicar/assinar). Ela fornece um mecanismo simples para um objeto informar um conjunto de objetos de terceiros interessados quando seu estado muda.
|
|
28
27
|
|
|
29
28
|
A biblioteca padrão do Ruby [tem uma abstração](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html) que permite usar esse padrão, mas seu design pode entrar em conflito com outras bibliotecas convencionais, como [`ActiveModel`/`ActiveRecord`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changed), que também tem o método [`changed`](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html#method-i-changed). Nesse caso, o comportamento ficaria comprometido por conta dessa sobrescrita de métodos.
|
|
@@ -45,13 +44,14 @@ Por causa desse problema, decidi criar uma gem que encapsula o padrão sem alter
|
|
|
45
44
|
- [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
|
|
46
45
|
- [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
|
|
47
46
|
- [Definindo observers com blocos](#definindo-observers-com-blocos)
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
47
|
+
- [`observers.on()`](#observerson)
|
|
48
|
+
- [`observers.once()`](#observersonce)
|
|
50
49
|
- [Substituindo um bloco por um `lambda`/`proc`](#substituindo-um-bloco-por-um-lambdaproc)
|
|
51
50
|
- [Desanexando observers](#desanexando-observers)
|
|
52
51
|
- [Integrações ActiveRecord e ActiveModel](#integrações-activerecord-e-activemodel)
|
|
53
|
-
- [notify_observers_on()](#notify_observers_on)
|
|
54
|
-
- [notify_observers()](#notify_observers)
|
|
52
|
+
- [`.notify_observers_on()`](#notify_observers_on)
|
|
53
|
+
- [Anexando observers no nível da classe (`.notify_observers!()`)](#anexando-observers-no-nível-da-classe-notify_observers)
|
|
54
|
+
- [`.notify_observers()`](#notify_observers)
|
|
55
55
|
- [Desenvolvimento](#desenvolvimento)
|
|
56
56
|
- [Contribuindo](#contribuindo)
|
|
57
57
|
- [License](#license)
|
|
@@ -67,11 +67,24 @@ gem 'u-observers'
|
|
|
67
67
|
|
|
68
68
|
# Compatibilidade
|
|
69
69
|
|
|
70
|
-
| u-observers | branch
|
|
71
|
-
| ----------- |
|
|
72
|
-
|
|
|
73
|
-
| 2.3.0 | v2.x
|
|
74
|
-
| 1.0.0 | v1.x
|
|
70
|
+
| u-observers | branch | ruby | activerecord |
|
|
71
|
+
| ----------- | ------ | -------- | --------------- |
|
|
72
|
+
| 3.0.0 | main | >= 2.7.0 | >= 6.0, <= Edge |
|
|
73
|
+
| 2.3.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
|
74
|
+
| 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
|
75
|
+
|
|
76
|
+
Esta biblioteca é testada (CI matrix) contra:
|
|
77
|
+
|
|
78
|
+
| Ruby / Rails | 6.0 | 6.1 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | Edge |
|
|
79
|
+
| ------------ | --- | --- | --- | --- | --- | --- | --- | ---- |
|
|
80
|
+
| 2.7 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
81
|
+
| 3.0 | ✅ | ✅ | ✅ | ✅ | | | | |
|
|
82
|
+
| 3.1 | | | ✅ | ✅ | ✅ | | | |
|
|
83
|
+
| 3.2 | | | ✅ | ✅ | ✅ | ✅ | | |
|
|
84
|
+
| 3.3 | | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
85
|
+
| 3.4 | | | | | ✅ | ✅ | ✅ | ✅ |
|
|
86
|
+
| 4.x | | | | | | | ✅ | ✅ |
|
|
87
|
+
| Head | | | | | | | ✅ | ✅ |
|
|
75
88
|
|
|
76
89
|
> **Nota**: O ActiveRecord não é uma dependência, mas você pode adicionar um módulo para habilitar alguns métodos estáticos que foram projetados para serem usados com seus [callbacks](https://guides.rubyonrails.org/active_record_callbacks.html).
|
|
77
90
|
|
|
@@ -158,7 +171,7 @@ order.observers.notify
|
|
|
158
171
|
|
|
159
172
|
### Compartilhando um contexto com seus observadores
|
|
160
173
|
|
|
161
|
-
Para compartilhar um valor de contexto (qualquer tipo de objeto Ruby) com um ou mais observadores, você precisará usar a palavra-chave `:context` como o último argumento do
|
|
174
|
+
Para compartilhar um valor de contexto (qualquer tipo de objeto Ruby) com um ou mais observadores, você precisará usar a palavra-chave `:context` como o último argumento do método `#attach`. Este recurso oferece a você uma oportunidade única de compartilhar um valor no momento de anexar um _observer_.
|
|
162
175
|
|
|
163
176
|
Quando o método do observer receber dois argumentos, o primeiro será o sujeito e o segundo uma instância `Micro::Observers::Event` que terá o valor do contexto.
|
|
164
177
|
|
|
@@ -190,7 +203,7 @@ order.cancel!
|
|
|
190
203
|
|
|
191
204
|
### Compartilhando dados ao notificar os observadores
|
|
192
205
|
|
|
193
|
-
Como mencionado anteriormente, o [`event context`](#compartilhando-um-contexto-com-seus-observadores) é um valor armazenado quando você anexa seu
|
|
206
|
+
Como mencionado anteriormente, o [`event context`](#compartilhando-um-contexto-com-seus-observadores) é um valor armazenado quando você anexa seu _observer_. Mas, às vezes, será útil enviar alguns dados adicionais ao transmitir um evento aos seus _observers_. O `event data` dá a você esta oportunidade única de compartilhar algum valor no momento da notificação.
|
|
194
207
|
|
|
195
208
|
```ruby
|
|
196
209
|
class Order
|
|
@@ -217,12 +230,13 @@ order.observers.notify(:changed, data: 1)
|
|
|
217
230
|
### O que é `Micro::Observers::Event`?
|
|
218
231
|
|
|
219
232
|
O `Micro::Observers::Event` é o payload do evento. Veja abaixo todas as suas propriedades:
|
|
233
|
+
|
|
220
234
|
- `#name` será o evento transmitido.
|
|
221
235
|
- `#subject` será o sujeito observado.
|
|
222
|
-
- `#context` serão [os dados de contexto](#compartilhando-um-contexto-com-seus-observadores) que foram definidos no momento em que você anexa o
|
|
236
|
+
- `#context` serão [os dados de contexto](#compartilhando-um-contexto-com-seus-observadores) que foram definidos no momento em que você anexa o _observer_.
|
|
223
237
|
- `#data` será [o valor compartilhado na notificação dos observadores](#compartilhando-dados-ao-notificar-os-observadores).
|
|
224
238
|
- `#ctx` é um apelido para o método `#context`.
|
|
225
|
-
- `#subj` é um
|
|
239
|
+
- `#subj` é um _alias_ para o método `#subject`.
|
|
226
240
|
|
|
227
241
|
[⬆️ Voltar para o índice](#índice-)
|
|
228
242
|
|
|
@@ -233,10 +247,11 @@ O método `observers.on()` permite que você anexe um callable (objeto que respo
|
|
|
233
247
|
Normalmente, um callable tem uma responsabilidade bem definida (faz apenas uma coisa), por isso, tende a ser mais amigável com o [SRP (princípio de responsabilidade única)](https://en.wikipedia.org/wiki/Single-responsibility_principle) do que um observador convencional (que poderia ter N métodos para responder a diferentes tipos de notificação).
|
|
234
248
|
|
|
235
249
|
Este método recebe as opções abaixo:
|
|
250
|
+
|
|
236
251
|
1. `:event` o nome do evento esperado.
|
|
237
252
|
2. `:call` o próprio callable.
|
|
238
253
|
3. `:with` (opcional) pode definir o valor que será usado como argumento do objeto callable. Portanto, se for um `Proc`, uma instância de `Micro::Observers::Event` será recebida como o argumento `Proc` e sua saída será o argumento que pode ser chamado. Mas se essa opção não for definida, a instância `Micro::Observers::Event` será o argumento do callable.
|
|
239
|
-
4. `:context` serão os dados de contexto que foram definidos no momento em que você anexa o
|
|
254
|
+
4. `:context` serão os dados de contexto que foram definidos no momento em que você anexa o _observer_.
|
|
240
255
|
|
|
241
256
|
```ruby
|
|
242
257
|
class Person
|
|
@@ -280,7 +295,7 @@ person.name = 'Coutinho'
|
|
|
280
295
|
|
|
281
296
|
### Chamando os observadores
|
|
282
297
|
|
|
283
|
-
Você pode usar um callable (uma classe, módulo ou objeto que responda ao método `call`) para ser seu
|
|
298
|
+
Você pode usar um callable (uma classe, módulo ou objeto que responda ao método `call`) para ser seu _observer_. Para fazer isso, você só precisa usar o método `#call` em vez de `#notify`.
|
|
284
299
|
|
|
285
300
|
```ruby
|
|
286
301
|
class Order
|
|
@@ -383,7 +398,7 @@ order.cancel! # Nothing will happen because there aren't observers.
|
|
|
383
398
|
|
|
384
399
|
Os métodos `#on()` e `#once()` podem receber um evento (a `symbol`) e um bloco para definir observers.
|
|
385
400
|
|
|
386
|
-
####
|
|
401
|
+
#### `observers.on()`
|
|
387
402
|
|
|
388
403
|
```ruby
|
|
389
404
|
class Order
|
|
@@ -406,7 +421,7 @@ order.cancel! # The order #(70301497466060) has been canceled.
|
|
|
406
421
|
order.observers.some? # true
|
|
407
422
|
```
|
|
408
423
|
|
|
409
|
-
####
|
|
424
|
+
#### `observers.once()`
|
|
410
425
|
|
|
411
426
|
```ruby
|
|
412
427
|
class Order
|
|
@@ -431,7 +446,7 @@ order.observers.some? # false
|
|
|
431
446
|
|
|
432
447
|
#### Substituindo um bloco por um `lambda`/`proc`
|
|
433
448
|
|
|
434
|
-
Ruby permite que você substitua qualquer bloco com um `lambda`/`proc`. Exemplo:
|
|
449
|
+
Ruby permite que você substitua qualquer bloco com um `lambda`/`proc`. Portanto, será possível usar este tipo de recurso para definir seus observers. Exemplo:
|
|
435
450
|
|
|
436
451
|
```ruby
|
|
437
452
|
class Order
|
|
@@ -475,11 +490,12 @@ module OrderNotifications
|
|
|
475
490
|
end
|
|
476
491
|
|
|
477
492
|
order = Order.new
|
|
493
|
+
order.observers.on(:canceled) { |_event| }
|
|
478
494
|
order.observers.on(event: :canceled, call: NotifyAfterCancel)
|
|
479
495
|
order.observers.attach(OrderNotifications)
|
|
480
496
|
|
|
481
497
|
order.observers.some? # true
|
|
482
|
-
order.observers.count #
|
|
498
|
+
order.observers.count # 3
|
|
483
499
|
|
|
484
500
|
order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
|
|
485
501
|
order.observers.some? # true
|
|
@@ -497,15 +513,16 @@ order.observers.count # 0
|
|
|
497
513
|
Para fazer uso deste recurso, você precisa de um módulo adicional.
|
|
498
514
|
|
|
499
515
|
Exemplo de Gemfile:
|
|
516
|
+
|
|
500
517
|
```ruby
|
|
501
518
|
gem 'u-observers', require: 'u-observers/for/active_record'
|
|
502
519
|
```
|
|
503
520
|
|
|
504
521
|
Este recurso irá expor módulos que podem ser usados para adicionar macros (métodos estáticos) que foram projetados para funcionar com os callbacks do `ActiveModel`/`ActiveRecord`. Exemplo:
|
|
505
522
|
|
|
506
|
-
#### notify_observers_on()
|
|
523
|
+
#### `.notify_observers_on()`
|
|
507
524
|
|
|
508
|
-
O `notify_observers_on` permite que você defina um ou mais callbacks do `ActiveModel`/`ActiveRecord`, que serão usados para notificar seus
|
|
525
|
+
O `notify_observers_on` permite que você defina um ou mais callbacks do `ActiveModel`/`ActiveRecord`, que serão usados para notificar seus _observers_.
|
|
509
526
|
|
|
510
527
|
```ruby
|
|
511
528
|
class Post < ActiveRecord::Base
|
|
@@ -546,7 +563,62 @@ end
|
|
|
546
563
|
|
|
547
564
|
[⬆️ Voltar para o índice](#índice-)
|
|
548
565
|
|
|
549
|
-
#### notify_observers()
|
|
566
|
+
#### Anexando observers no nível da classe (`.notify_observers!()`)
|
|
567
|
+
|
|
568
|
+
Enquanto o `notify_observers_on` apenas conecta o callback a um broadcast (você ainda precisa chamar `attach` em cada instância), o `notify_observers!` também **vincula os _observers_ ao modelo no nível da classe** através da opção obrigatória `with:` — assim você nunca chama `observers.attach`. A opção `event:` nomeia o callback a ser usado; use `context:` para encaminhar um contexto para esses _observers_, e passe qualquer opção extra (por exemplo, `on:`) diretamente para o callback subjacente.
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
class Post < ActiveRecord::Base
|
|
572
|
+
include ::Micro::Observers::For::ActiveRecord
|
|
573
|
+
|
|
574
|
+
# Anexa TitlePrinter (e TitlePrinterWithContext) em todo after_commit
|
|
575
|
+
# disparado por um update — sem precisar de `observers.attach` por instância.
|
|
576
|
+
notify_observers!(
|
|
577
|
+
on: :update,
|
|
578
|
+
with: [TitlePrinter, TitlePrinterWithContext],
|
|
579
|
+
event: :after_commit,
|
|
580
|
+
context: { from: 'class-level' }
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Equivalente a:
|
|
584
|
+
#
|
|
585
|
+
# after_commit(on: :update) do |record|
|
|
586
|
+
# record.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'class-level' })
|
|
587
|
+
# record.observers.subject_changed!
|
|
588
|
+
# record.observers.notify(:after_commit)
|
|
589
|
+
# end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
Post.transaction { Post.create(title: 'Hello world') } # nada — `on: :update`
|
|
593
|
+
|
|
594
|
+
post = Post.first
|
|
595
|
+
Post.transaction { post.update(title: 'Hello again') }
|
|
596
|
+
# Title: Hello again
|
|
597
|
+
# Title: Hello again (de: class-level)
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
> **Nota**: `event:` e `with:` são obrigatórios (`with:` aceita um único _observer_ ou um array). Sem _observers_ para anexar, use o `notify_observers_on`.
|
|
601
|
+
|
|
602
|
+
Os _observers_ declarados são inspecionáveis e removíveis no nível da classe:
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
Post.observers_to_notify
|
|
606
|
+
# { after_commit: [TitlePrinter, TitlePrinterWithContext] }
|
|
607
|
+
|
|
608
|
+
# Para de notificar um dado observer (de todos os callbacks, ou use `from:` para limitar)
|
|
609
|
+
Post.detach_observers_to_notify(TitlePrinterWithContext)
|
|
610
|
+
# { after_commit: [TitlePrinter] }
|
|
611
|
+
|
|
612
|
+
Post.detach_observers_to_notify(TitlePrinter, from: :after_commit)
|
|
613
|
+
# {}
|
|
614
|
+
|
|
615
|
+
# Sem observers, remove o(s) callback(s) por completo:
|
|
616
|
+
Post.detach_observers_to_notify(from: :after_commit)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
[⬆️ Voltar para o índice](#índice-)
|
|
620
|
+
|
|
621
|
+
#### `.notify_observers()`
|
|
550
622
|
|
|
551
623
|
O `notify_observers` permite definir um ou mais eventos, que serão utilizados para notificar após a execução de algum callback do `ActiveModel`/`ActiveRecord`.
|
|
552
624
|
|
|
@@ -564,12 +636,6 @@ class Post < ActiveRecord::Base
|
|
|
564
636
|
# end
|
|
565
637
|
end
|
|
566
638
|
|
|
567
|
-
module TitlePrinter
|
|
568
|
-
def self.transaction_completed(post)
|
|
569
|
-
puts("Title: #{post.title}")
|
|
570
|
-
end
|
|
571
|
-
end
|
|
572
|
-
|
|
573
639
|
module TitlePrinterWithContext
|
|
574
640
|
def self.transaction_completed(post, event)
|
|
575
641
|
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
|
@@ -578,7 +644,11 @@ end
|
|
|
578
644
|
|
|
579
645
|
Post.transaction do
|
|
580
646
|
post = Post.new(title: 'Olá mundo')
|
|
581
|
-
|
|
647
|
+
|
|
648
|
+
post.observers.on(:transaction_completed) { |event| puts("Title: #{event.subject.title}") }
|
|
649
|
+
|
|
650
|
+
post.observers.attach(TitlePrinterWithContext, context: { from: 'example #7' })
|
|
651
|
+
|
|
582
652
|
post.save
|
|
583
653
|
end
|
|
584
654
|
|
|
@@ -599,7 +669,7 @@ Para instalar esta gem em sua máquina local, execute `bundle exec rake install`
|
|
|
599
669
|
|
|
600
670
|
## Contribuindo
|
|
601
671
|
|
|
602
|
-
Reportar bugs e solicitações de pull-requests são bem-vindos no GitHub em https://github.com/
|
|
672
|
+
Reportar bugs e solicitações de pull-requests são bem-vindos no GitHub em https://github.com/u-gems/u-observers. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e espera-se que os colaboradores sigam o [código de conduta](https://github.com/u-gems/u-observers/blob/master/CODE_OF_CONDUCT.md).
|
|
603
673
|
|
|
604
674
|
## License
|
|
605
675
|
|
|
@@ -607,4 +677,4 @@ A gem está disponível como código aberto sob os termos da [Licença MIT](http
|
|
|
607
677
|
|
|
608
678
|
## Código de conduta
|
|
609
679
|
|
|
610
|
-
Espera-se que todos que interagem nas bases de código do projeto `Micro::Observers`, rastreadores de problemas, salas de bate-papo e listas de discussão sigam o [código de conduta](https://github.com/
|
|
680
|
+
Espera-se que todos que interagem nas bases de código do projeto `Micro::Observers`, rastreadores de problemas, salas de bate-papo e listas de discussão sigam o [código de conduta](https://github.com/u-gems/u-observers/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -7,4 +7,34 @@ Rake::TestTask.new(:test) do |t|
|
|
|
7
7
|
t.test_files = FileList['test/**/*_test.rb']
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
task
|
|
10
|
+
require 'appraisal/task'
|
|
11
|
+
|
|
12
|
+
Appraisal::Task.new
|
|
13
|
+
|
|
14
|
+
desc 'Run the full test suite against every supported Rails version'
|
|
15
|
+
task :matrix do
|
|
16
|
+
appraisals =
|
|
17
|
+
if RUBY_VERSION < '3.1'
|
|
18
|
+
%w[rails-6-0 rails-6-1 rails-7-0 rails-7-1]
|
|
19
|
+
elsif RUBY_VERSION < '3.2'
|
|
20
|
+
%w[rails-7-0 rails-7-1 rails-7-2]
|
|
21
|
+
elsif RUBY_VERSION < '3.3'
|
|
22
|
+
%w[rails-7-0 rails-7-1 rails-7-2 rails-8-0]
|
|
23
|
+
elsif RUBY_VERSION < '3.4'
|
|
24
|
+
%w[rails-7-0 rails-7-1 rails-7-2 rails-8-0 rails-8-1 rails-edge]
|
|
25
|
+
elsif RUBY_VERSION < '4.0'
|
|
26
|
+
%w[rails-7-2 rails-8-0 rails-8-1 rails-edge]
|
|
27
|
+
else
|
|
28
|
+
%w[rails-8-1 rails-edge]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Baseline (no activerecord)
|
|
32
|
+
sh 'bundle exec rake test'
|
|
33
|
+
|
|
34
|
+
# Each activerecord appraisal
|
|
35
|
+
appraisals.each do |appraisal|
|
|
36
|
+
sh "bundle exec appraisal #{appraisal} rake test"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
task default: :test
|