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.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.
|
|
@@ -44,10 +43,15 @@ Por causa desse problema, decidi criar uma gem que encapsula o padrão sem alter
|
|
|
44
43
|
- [Definindo observers que executam apenas uma vez](#definindo-observers-que-executam-apenas-uma-vez)
|
|
45
44
|
- [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
|
|
46
45
|
- [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
|
|
46
|
+
- [Definindo observers com blocos](#definindo-observers-com-blocos)
|
|
47
|
+
- [`observers.on()`](#observerson)
|
|
48
|
+
- [`observers.once()`](#observersonce)
|
|
49
|
+
- [Substituindo um bloco por um `lambda`/`proc`](#substituindo-um-bloco-por-um-lambdaproc)
|
|
47
50
|
- [Desanexando observers](#desanexando-observers)
|
|
48
51
|
- [Integrações ActiveRecord e ActiveModel](#integrações-activerecord-e-activemodel)
|
|
49
|
-
- [notify_observers_on()](#notify_observers_on)
|
|
50
|
-
- [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)
|
|
51
55
|
- [Desenvolvimento](#desenvolvimento)
|
|
52
56
|
- [Contribuindo](#contribuindo)
|
|
53
57
|
- [License](#license)
|
|
@@ -63,11 +67,24 @@ gem 'u-observers'
|
|
|
63
67
|
|
|
64
68
|
# Compatibilidade
|
|
65
69
|
|
|
66
|
-
| u-observers | branch
|
|
67
|
-
| ----------- |
|
|
68
|
-
|
|
|
69
|
-
| 2.
|
|
70
|
-
| 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 | | | | | | | ✅ | ✅ |
|
|
71
88
|
|
|
72
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).
|
|
73
90
|
|
|
@@ -154,7 +171,7 @@ order.observers.notify
|
|
|
154
171
|
|
|
155
172
|
### Compartilhando um contexto com seus observadores
|
|
156
173
|
|
|
157
|
-
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_.
|
|
158
175
|
|
|
159
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.
|
|
160
177
|
|
|
@@ -186,7 +203,7 @@ order.cancel!
|
|
|
186
203
|
|
|
187
204
|
### Compartilhando dados ao notificar os observadores
|
|
188
205
|
|
|
189
|
-
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.
|
|
190
207
|
|
|
191
208
|
```ruby
|
|
192
209
|
class Order
|
|
@@ -213,12 +230,13 @@ order.observers.notify(:changed, data: 1)
|
|
|
213
230
|
### O que é `Micro::Observers::Event`?
|
|
214
231
|
|
|
215
232
|
O `Micro::Observers::Event` é o payload do evento. Veja abaixo todas as suas propriedades:
|
|
233
|
+
|
|
216
234
|
- `#name` será o evento transmitido.
|
|
217
235
|
- `#subject` será o sujeito observado.
|
|
218
|
-
- `#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_.
|
|
219
237
|
- `#data` será [o valor compartilhado na notificação dos observadores](#compartilhando-dados-ao-notificar-os-observadores).
|
|
220
238
|
- `#ctx` é um apelido para o método `#context`.
|
|
221
|
-
- `#subj` é um
|
|
239
|
+
- `#subj` é um _alias_ para o método `#subject`.
|
|
222
240
|
|
|
223
241
|
[⬆️ Voltar para o índice](#índice-)
|
|
224
242
|
|
|
@@ -229,10 +247,11 @@ O método `observers.on()` permite que você anexe um callable (objeto que respo
|
|
|
229
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).
|
|
230
248
|
|
|
231
249
|
Este método recebe as opções abaixo:
|
|
250
|
+
|
|
232
251
|
1. `:event` o nome do evento esperado.
|
|
233
252
|
2. `:call` o próprio callable.
|
|
234
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.
|
|
235
|
-
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_.
|
|
236
255
|
|
|
237
256
|
```ruby
|
|
238
257
|
class Person
|
|
@@ -276,7 +295,7 @@ person.name = 'Coutinho'
|
|
|
276
295
|
|
|
277
296
|
### Chamando os observadores
|
|
278
297
|
|
|
279
|
-
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`.
|
|
280
299
|
|
|
281
300
|
```ruby
|
|
282
301
|
class Order
|
|
@@ -373,7 +392,84 @@ order.observers.some? # false
|
|
|
373
392
|
order.cancel! # Nothing will happen because there aren't observers.
|
|
374
393
|
```
|
|
375
394
|
|
|
376
|
-
[⬆️
|
|
395
|
+
[⬆️ Voltar para o índice](#índice-)
|
|
396
|
+
|
|
397
|
+
### Definindo observers com blocos
|
|
398
|
+
|
|
399
|
+
Os métodos `#on()` e `#once()` podem receber um evento (a `symbol`) e um bloco para definir 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
|
+
#### Substituindo um bloco por um `lambda`/`proc`
|
|
448
|
+
|
|
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:
|
|
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
|
+
[⬆️ Voltar para o índice](#índice-)
|
|
377
473
|
|
|
378
474
|
### Desanexando observers
|
|
379
475
|
|
|
@@ -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
|
|
@@ -409,22 +506,23 @@ order.observers.some? # false
|
|
|
409
506
|
order.observers.count # 0
|
|
410
507
|
```
|
|
411
508
|
|
|
412
|
-
[⬆️
|
|
509
|
+
[⬆️ Voltar para o índice](#índice-)
|
|
413
510
|
|
|
414
511
|
### Integrações ActiveRecord e ActiveModel
|
|
415
512
|
|
|
416
513
|
Para fazer uso deste recurso, você precisa de um módulo adicional.
|
|
417
514
|
|
|
418
515
|
Exemplo de Gemfile:
|
|
516
|
+
|
|
419
517
|
```ruby
|
|
420
518
|
gem 'u-observers', require: 'u-observers/for/active_record'
|
|
421
519
|
```
|
|
422
520
|
|
|
423
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:
|
|
424
522
|
|
|
425
|
-
#### notify_observers_on()
|
|
523
|
+
#### `.notify_observers_on()`
|
|
426
524
|
|
|
427
|
-
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_.
|
|
428
526
|
|
|
429
527
|
```ruby
|
|
430
528
|
class Post < ActiveRecord::Base
|
|
@@ -465,7 +563,62 @@ end
|
|
|
465
563
|
|
|
466
564
|
[⬆️ Voltar para o índice](#índice-)
|
|
467
565
|
|
|
468
|
-
#### 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()`
|
|
469
622
|
|
|
470
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`.
|
|
471
624
|
|
|
@@ -483,12 +636,6 @@ class Post < ActiveRecord::Base
|
|
|
483
636
|
# end
|
|
484
637
|
end
|
|
485
638
|
|
|
486
|
-
module TitlePrinter
|
|
487
|
-
def self.transaction_completed(post)
|
|
488
|
-
puts("Title: #{post.title}")
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
639
|
module TitlePrinterWithContext
|
|
493
640
|
def self.transaction_completed(post, event)
|
|
494
641
|
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
|
@@ -497,7 +644,11 @@ end
|
|
|
497
644
|
|
|
498
645
|
Post.transaction do
|
|
499
646
|
post = Post.new(title: 'Olá mundo')
|
|
500
|
-
|
|
647
|
+
|
|
648
|
+
post.observers.on(:transaction_completed) { |event| puts("Title: #{event.subject.title}") }
|
|
649
|
+
|
|
650
|
+
post.observers.attach(TitlePrinterWithContext, context: { from: 'example #7' })
|
|
651
|
+
|
|
501
652
|
post.save
|
|
502
653
|
end
|
|
503
654
|
|
|
@@ -518,7 +669,7 @@ Para instalar esta gem em sua máquina local, execute `bundle exec rake install`
|
|
|
518
669
|
|
|
519
670
|
## Contribuindo
|
|
520
671
|
|
|
521
|
-
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).
|
|
522
673
|
|
|
523
674
|
## License
|
|
524
675
|
|
|
@@ -526,4 +677,4 @@ A gem está disponível como código aberto sob os termos da [Licença MIT](http
|
|
|
526
677
|
|
|
527
678
|
## Código de conduta
|
|
528
679
|
|
|
529
|
-
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
|
data/bin/matrix
ADDED
data/bin/setup
CHANGED
|
@@ -6,7 +6,7 @@ module Micro
|
|
|
6
6
|
|
|
7
7
|
module ActiveModel
|
|
8
8
|
module ClassMethods
|
|
9
|
-
def
|
|
9
|
+
def notify_observers_proc(events)
|
|
10
10
|
proc do |object|
|
|
11
11
|
object.observers.subject_changed!
|
|
12
12
|
object.observers.send(:broadcast_if_subject_changed, events)
|
|
@@ -14,19 +14,133 @@ module Micro
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def notify_observers(*events)
|
|
17
|
-
|
|
17
|
+
notify_observers_proc(Event::Names.fetch(events))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def notify_observers_on(*callback_methods)
|
|
21
21
|
Utils::Arrays.fetch_from_args(callback_methods).each do |callback_method|
|
|
22
|
-
self.public_send(callback_method, &
|
|
22
|
+
self.public_send(callback_method, ¬ify_observers_proc([callback_method]))
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
NO_OBSERVERS_TO_NOTIFY_MSG =
|
|
27
|
+
'no observers (expected at least one observer in `with:`)'.freeze
|
|
28
|
+
|
|
29
|
+
NO_EVENT_TO_NOTIFY_MSG =
|
|
30
|
+
'no event (expected a callback name in `event:`)'.freeze
|
|
31
|
+
|
|
32
|
+
def notify_observers!(event:, with:, context: nil, **callback_options)
|
|
33
|
+
observers = Utils::Arrays.flatten_and_compact(with)
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, NO_OBSERVERS_TO_NOTIFY_MSG if observers.empty?
|
|
36
|
+
|
|
37
|
+
events = Utils::Arrays.flatten_and_compact(event)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, NO_EVENT_TO_NOTIFY_MSG if events.empty?
|
|
40
|
+
|
|
41
|
+
events.each do |callback_method|
|
|
42
|
+
register_observers_to_notify(callback_method, observers, context)
|
|
43
|
+
|
|
44
|
+
install_observers_to_notify_callback(callback_method, callback_options)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Introspection: the observers declared via `notify_observers!`, keyed
|
|
49
|
+
# by the callback they fire on. e.g. { after_commit: [TitlePrinter] }
|
|
50
|
+
def observers_to_notify
|
|
51
|
+
__observers_to_notify.each_with_object({}) do |(event, entries), result|
|
|
52
|
+
result[event] = entries.map { |observer, _context| observer } unless entries.empty?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Remove previously declared observers. Without `from:` they are
|
|
57
|
+
# removed from every callback; pass `from:` (a callback name or an
|
|
58
|
+
# array of them) to scope it. With no observers, clears the callback(s).
|
|
59
|
+
def detach_observers_to_notify(*observers, from: nil)
|
|
60
|
+
observers = Utils::Arrays.flatten_and_compact(observers)
|
|
61
|
+
events = from ? Utils::Arrays.flatten_and_compact(from) : __observers_to_notify.keys
|
|
62
|
+
|
|
63
|
+
events.each do |event|
|
|
64
|
+
entries = __observers_to_notify[event]
|
|
65
|
+
|
|
66
|
+
next unless entries
|
|
67
|
+
|
|
68
|
+
if observers.empty?
|
|
69
|
+
__observers_to_notify.delete(event)
|
|
70
|
+
else
|
|
71
|
+
entries.reject! { |observer, _context| observers.include?(observer) }
|
|
72
|
+
__observers_to_notify.delete(event) if entries.empty?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
observers_to_notify
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def register_observers_to_notify(event, observers, context)
|
|
80
|
+
entries = (__observers_to_notify[event] ||= [])
|
|
81
|
+
|
|
82
|
+
observers.each do |observer|
|
|
83
|
+
entries << [observer, context] unless entries.any? { |existing, _| existing == observer }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def install_observers_to_notify_callback(event, callback_options)
|
|
88
|
+
installed = __observers_to_notify_callbacks
|
|
89
|
+
|
|
90
|
+
return if installed[event]
|
|
91
|
+
|
|
92
|
+
installed[event] = true
|
|
93
|
+
|
|
94
|
+
declaring_class = self
|
|
95
|
+
|
|
96
|
+
callback_block = proc do |record|
|
|
97
|
+
declaring_class.send(:notify_registered_observers, record, event)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if callback_options.empty?
|
|
101
|
+
self.public_send(event, &callback_block)
|
|
102
|
+
else
|
|
103
|
+
self.public_send(event, **callback_options, &callback_block)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def notify_registered_observers(record, event)
|
|
108
|
+
entries = __observers_to_notify[event]
|
|
109
|
+
|
|
110
|
+
return if entries.nil? || entries.empty?
|
|
111
|
+
|
|
112
|
+
set = record.observers
|
|
113
|
+
|
|
114
|
+
entries.each do |observer, context|
|
|
115
|
+
context ? set.attach(observer, context: context) : set.attach(observer)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
set.subject_changed!
|
|
119
|
+
set.send(:broadcast_if_subject_changed, [event])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def __observers_to_notify
|
|
123
|
+
@__observers_to_notify ||= {}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def __observers_to_notify_callbacks
|
|
127
|
+
@__observers_to_notify_callbacks ||= {}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private_constant :NO_OBSERVERS_TO_NOTIFY_MSG, :NO_EVENT_TO_NOTIFY_MSG
|
|
25
131
|
end
|
|
26
132
|
|
|
27
133
|
def self.included(base)
|
|
28
134
|
base.extend(ClassMethods)
|
|
29
|
-
base.send(
|
|
135
|
+
base.send(
|
|
136
|
+
:private_class_method,
|
|
137
|
+
:notify_observers_proc,
|
|
138
|
+
:register_observers_to_notify,
|
|
139
|
+
:install_observers_to_notify_callback,
|
|
140
|
+
:notify_registered_observers,
|
|
141
|
+
:__observers_to_notify,
|
|
142
|
+
:__observers_to_notify_callbacks
|
|
143
|
+
)
|
|
30
144
|
base.send(:include, ::Micro::Observers)
|
|
31
145
|
end
|
|
32
146
|
end
|
data/lib/micro/observers/set.rb
CHANGED
|
@@ -42,13 +42,20 @@ module Micro
|
|
|
42
42
|
def attach(*args); @subscribers.attach(args) and self; end
|
|
43
43
|
def detach(*args); @subscribers.detach(args) and self; end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
CallableOptions = -> (arg, block) do
|
|
46
|
+
arg.is_a?(Symbol) && block ? { event: arg, call: block } : arg
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on(arg = Utils::EMPTY_HASH, &block)
|
|
50
|
+
@subscribers.on(CallableOptions[arg, block]) and self
|
|
51
|
+
end
|
|
47
52
|
|
|
48
|
-
def
|
|
49
|
-
@subscribers.
|
|
53
|
+
def once(arg = Utils::EMPTY_HASH, &block)
|
|
54
|
+
@subscribers.once(CallableOptions[arg, block]) and self
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
def off(*args); @subscribers.off(args) and self; end
|
|
58
|
+
|
|
52
59
|
def notify(*events, data: nil)
|
|
53
60
|
broadcast_if_subject_changed(Event::Names.fetch(events), data)
|
|
54
61
|
end
|
|
@@ -93,7 +100,7 @@ module Micro
|
|
|
93
100
|
self
|
|
94
101
|
end
|
|
95
102
|
|
|
96
|
-
private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT
|
|
103
|
+
private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT; :CallableOptions
|
|
97
104
|
end
|
|
98
105
|
|
|
99
106
|
end
|
data/u-observers.gemspec
CHANGED
|
@@ -8,12 +8,13 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
|
|
9
9
|
spec.summary = %q{Simple and powerful implementation of the observer pattern.}
|
|
10
10
|
spec.description = %q{Simple and powerful implementation of the observer pattern.}
|
|
11
|
-
spec.homepage = 'https://github.com/
|
|
11
|
+
spec.homepage = 'https://github.com/u-gems/u-observers'
|
|
12
12
|
spec.license = 'MIT'
|
|
13
13
|
|
|
14
14
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
15
|
-
spec.metadata['source_code_uri'] = 'https://github.com/
|
|
16
|
-
spec.metadata['changelog_uri'] = 'https://github.com/
|
|
15
|
+
spec.metadata['source_code_uri'] = 'https://github.com/u-gems/u-observers'
|
|
16
|
+
spec.metadata['changelog_uri'] = 'https://github.com/u-gems/u-observers/blob/main/CHANGELOG.md'
|
|
17
|
+
spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
|
|
17
18
|
|
|
18
19
|
# Specify which files should be added to the gem when it is released.
|
|
19
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
@@ -24,8 +25,9 @@ Gem::Specification.new do |spec|
|
|
|
24
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
25
26
|
spec.require_paths = ['lib']
|
|
26
27
|
|
|
27
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.
|
|
28
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
|
28
29
|
|
|
30
|
+
spec.add_development_dependency 'appraisal', '~> 2.5'
|
|
29
31
|
spec.add_development_dependency 'bundler'
|
|
30
32
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
31
33
|
end
|