u-observers 2.2.1 β 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 +190 -40
- data/README.pt-BR.md +193 -42
- 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/set.rb +12 -5
- 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)
|
|
@@ -45,10 +45,15 @@ Because of this issue, I decided to create a gem that encapsulates the pattern w
|
|
|
45
45
|
- [Defining observers that execute only once](#defining-observers-that-execute-only-once)
|
|
46
46
|
- [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
|
|
47
47
|
- [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
|
|
48
|
+
- [Defining observers using blocks](#defining-observers-using-blocks)
|
|
49
|
+
- [`observers.on()`](#observerson)
|
|
50
|
+
- [`observers.once()`](#observersonce)
|
|
51
|
+
- [Replacing a block by a `lambda`/`proc`](#replacing-a-block-by-a-lambdaproc)
|
|
48
52
|
- [Detaching observers](#detaching-observers)
|
|
49
53
|
- [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
|
|
50
|
-
- [notify_observers_on()](#notify_observers_on)
|
|
51
|
-
- [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)
|
|
52
57
|
- [Development](#development)
|
|
53
58
|
- [Contributing](#contributing)
|
|
54
59
|
- [License](#license)
|
|
@@ -64,11 +69,24 @@ gem 'u-observers'
|
|
|
64
69
|
|
|
65
70
|
# Compatibility
|
|
66
71
|
|
|
67
|
-
| u-observers | branch
|
|
68
|
-
| ----------- |
|
|
69
|
-
|
|
|
70
|
-
| 2.
|
|
71
|
-
| 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 | | | | | | | β
| β
|
|
|
72
90
|
|
|
73
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).
|
|
74
92
|
|
|
@@ -215,8 +233,8 @@ order.observers.notify(:changed, data: 1)
|
|
|
215
233
|
The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
|
|
216
234
|
|
|
217
235
|
- `#name` will be the broadcasted event.
|
|
218
|
-
- `#subject` will be the observed
|
|
219
|
-
- `#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.
|
|
220
238
|
- `#data` will be [the value that was shared in the observers' notification](#sharing-data-when-notifying-the-observers).
|
|
221
239
|
- `#ctx` is an alias for the `#context` method.
|
|
222
240
|
- `#subj` is an alias for the `#subject` method.
|
|
@@ -227,9 +245,10 @@ The `Micro::Observers::Event` is the event payload. Follow below all of its prop
|
|
|
227
245
|
|
|
228
246
|
The `observers.on()` method enables you to attach a callable as an observer.
|
|
229
247
|
|
|
230
|
-
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).
|
|
231
249
|
|
|
232
250
|
This method receives the below options:
|
|
251
|
+
|
|
233
252
|
1. `:event` the expected event name.
|
|
234
253
|
2. `:call` the callable object itself.
|
|
235
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.
|
|
@@ -375,6 +394,83 @@ order.cancel! # Nothing will happen because there aren't observers.
|
|
|
375
394
|
|
|
376
395
|
[β¬οΈ Back to Top](#table-of-contents-)
|
|
377
396
|
|
|
397
|
+
### Defining observers using blocks
|
|
398
|
+
|
|
399
|
+
The methods `#on()` and `#once()` can receive an event (`symbol`) and a block to define observers.
|
|
400
|
+
|
|
401
|
+
#### `observers.on()`
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
class Order
|
|
405
|
+
include Micro::Observers
|
|
406
|
+
|
|
407
|
+
def cancel!
|
|
408
|
+
observers.notify!(:canceled)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
order = Order.new
|
|
413
|
+
order.observers.on(:canceled) do |event|
|
|
414
|
+
puts "The order #(#{event.subject.object_id}) has been canceled."
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
order.observers.some? # true
|
|
418
|
+
|
|
419
|
+
order.cancel! # The order #(70301497466060) has been canceled.
|
|
420
|
+
|
|
421
|
+
order.observers.some? # true
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### `observers.once()`
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
class Order
|
|
428
|
+
include Micro::Observers
|
|
429
|
+
|
|
430
|
+
def cancel!
|
|
431
|
+
observers.notify!(:canceled)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
order = Order.new
|
|
436
|
+
order.observers.once(:canceled) do |event|
|
|
437
|
+
puts "The order #(#{event.subject.object_id}) has been canceled."
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
order.observers.some? # true
|
|
441
|
+
|
|
442
|
+
order.cancel! # The order #(70301497466060) has been canceled.
|
|
443
|
+
|
|
444
|
+
order.observers.some? # false
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### Replacing a block by a `lambda`/`proc`
|
|
448
|
+
|
|
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.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
class Order
|
|
453
|
+
include Micro::Observers
|
|
454
|
+
|
|
455
|
+
def cancel!
|
|
456
|
+
observers.notify!(:canceled)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
NotifyAfterCancel = -> event { puts "The order #(#{event.subject.object_id}) has been canceled." }
|
|
461
|
+
|
|
462
|
+
order = Order.new
|
|
463
|
+
order.observers.once(:canceled, &NotifyAfterCancel)
|
|
464
|
+
|
|
465
|
+
order.observers.some? # true
|
|
466
|
+
order.cancel! # The order #(70301497466060) has been canceled.
|
|
467
|
+
|
|
468
|
+
order.observers.some? # false
|
|
469
|
+
order.cancel! # Nothing will happen because there aren't observers.
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
[β¬οΈ Back to Top](#table-of-contents-)
|
|
473
|
+
|
|
378
474
|
### Detaching observers
|
|
379
475
|
|
|
380
476
|
As shown in the first example, you can use the `observers.detach()` to remove observers.
|
|
@@ -394,11 +490,12 @@ module OrderNotifications
|
|
|
394
490
|
end
|
|
395
491
|
|
|
396
492
|
order = Order.new
|
|
493
|
+
order.observers.on(:canceled) { |_event| }
|
|
397
494
|
order.observers.on(event: :canceled, call: NotifyAfterCancel)
|
|
398
495
|
order.observers.attach(OrderNotifications)
|
|
399
496
|
|
|
400
497
|
order.observers.some? # true
|
|
401
|
-
order.observers.count #
|
|
498
|
+
order.observers.count # 3
|
|
402
499
|
|
|
403
500
|
order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
|
|
404
501
|
order.observers.some? # true
|
|
@@ -416,15 +513,16 @@ order.observers.count # 0
|
|
|
416
513
|
To make use of this feature you need to require an additional module.
|
|
417
514
|
|
|
418
515
|
Gemfile example:
|
|
516
|
+
|
|
419
517
|
```ruby
|
|
420
518
|
gem 'u-observers', require: 'u-observers/for/active_record'
|
|
421
519
|
```
|
|
422
520
|
|
|
423
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:
|
|
424
522
|
|
|
425
|
-
#### notify_observers_on()
|
|
523
|
+
#### `.notify_observers_on()`
|
|
426
524
|
|
|
427
|
-
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.
|
|
428
526
|
|
|
429
527
|
```ruby
|
|
430
528
|
class Post < ActiveRecord::Base
|
|
@@ -464,9 +562,64 @@ end
|
|
|
464
562
|
|
|
465
563
|
[β¬οΈ Back to Top](#table-of-contents-)
|
|
466
564
|
|
|
467
|
-
#### notify_observers()
|
|
565
|
+
#### Attaching observers at the class level (`.notify_observers!()`)
|
|
468
566
|
|
|
469
|
-
|
|
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()`
|
|
621
|
+
|
|
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.
|
|
470
623
|
|
|
471
624
|
```ruby
|
|
472
625
|
class Post < ActiveRecord::Base
|
|
@@ -482,12 +635,6 @@ class Post < ActiveRecord::Base
|
|
|
482
635
|
# end
|
|
483
636
|
end
|
|
484
637
|
|
|
485
|
-
module TitlePrinter
|
|
486
|
-
def self.transaction_completed(post)
|
|
487
|
-
puts("Title: #{post.title}")
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
638
|
module TitlePrinterWithContext
|
|
492
639
|
def self.transaction_completed(post, event)
|
|
493
640
|
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
|
@@ -496,7 +643,11 @@ end
|
|
|
496
643
|
|
|
497
644
|
Post.transaction do
|
|
498
645
|
post = Post.new(title: 'OlΓ‘ mundo')
|
|
499
|
-
|
|
646
|
+
|
|
647
|
+
post.observers.on(:transaction_completed) { |event| puts("Title: #{event.subject.title}") }
|
|
648
|
+
|
|
649
|
+
post.observers.attach(TitlePrinterWithContext, context: { from: 'example #7' })
|
|
650
|
+
|
|
500
651
|
post.save
|
|
501
652
|
end
|
|
502
653
|
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
|
@@ -516,8 +667,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
516
667
|
|
|
517
668
|
## Contributing
|
|
518
669
|
|
|
519
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
520
|
-
|
|
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).
|
|
521
671
|
|
|
522
672
|
## License
|
|
523
673
|
|
|
@@ -525,4 +675,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
525
675
|
|
|
526
676
|
## Code of Conduct
|
|
527
677
|
|
|
528
|
-
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).
|