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.
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
- <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>
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 | ruby | activerecord |
67
- | ----------- | ------- | -------- | ------------- |
68
- | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
69
- | 2.2.1 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
70
- | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
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 método `#attach`. Este recurso oferece a você uma oportunidade única de compartilhar um valor no momento de anexar um *observer*.
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 *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.
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 *observer*.
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 *alias* para o método `#subject`.
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 *observer*.
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 *observer*. Para fazer isso, você só precisa usar o método `#call` em vez de `#notify`.
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
- [⬆️ &nbsp; Back to Top](#table-of-contents-)
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 # 2
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
- [⬆️ &nbsp; Back to Top](#table-of-contents-)
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 *observers*.
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
- post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
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/serradura/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/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
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/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
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 :default => :test
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
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ ruby -v
7
+
8
+ rm -f Gemfile.lock
9
+
10
+ bundle install
11
+
12
+ bundle exec appraisal update
13
+
14
+ bundle exec rake matrix
15
+
16
+ ruby -v
data/bin/setup CHANGED
@@ -3,6 +3,10 @@ set -euo pipefail
3
3
  IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
+ rm -f Gemfile.lock
7
+
6
8
  bundle install
7
9
 
10
+ bundle exec appraisal install
11
+
8
12
  # Do any other automated setup that you need to do here
@@ -6,7 +6,7 @@ module Micro
6
6
 
7
7
  module ActiveModel
8
8
  module ClassMethods
9
- def notify_observers!(events)
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
- notify_observers!(Event::Names.fetch(events))
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, &notify_observers!([callback_method]))
22
+ self.public_send(callback_method, &notify_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(:private_class_method, :notify_observers!)
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
@@ -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
- def on(options = Utils::EMPTY_HASH); @subscribers.on(options) and self; end
46
- def once(options = Utils::EMPTY_HASH); @subscribers.once(options) and self; end
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 off(*args)
49
- @subscribers.off(args) and self
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
@@ -1,5 +1,5 @@
1
1
  module Micro
2
2
  module Observers
3
- VERSION = '2.2.1'
3
+ VERSION = '3.0.0'
4
4
  end
5
5
  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/serradura/u-observers'
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/serradura/u-observers'
16
- spec.metadata['changelog_uri'] = 'https://github.com/serradura/u-observers/releases'
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.2.0')
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