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.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
- <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
+ > **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? πŸ‡§πŸ‡·&nbsp;πŸ‡΅πŸ‡Ή Verifique o [README traduzido em pt-BR](https://github.com/serradura/u-observers/blob/main/README.pt-BR.md).
32
+ > **Note:** VocΓͺ entende portuguΓͺs? πŸ‡§πŸ‡·&nbsp;πŸ‡΅πŸ‡Ή 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 | ruby | activerecord |
68
- | ----------- | ------- | -------- | ------------- |
69
- | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
70
- | 2.2.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
71
- | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
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 subject.
219
- - `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined in the moment that you attach the observer.
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). friendly than a conventional observer (that could have N methods to respond to different kinds of notification).
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
  [⬆️ &nbsp; 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
+ [⬆️ &nbsp; 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 # 2
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 object observers.
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
  [⬆️ &nbsp; Back to Top](#table-of-contents-)
466
564
 
467
- #### notify_observers()
565
+ #### Attaching observers at the class level (`.notify_observers!()`)
468
566
 
469
- 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.
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
+ [⬆️ &nbsp; 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
- post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
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/serradura/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/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
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/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
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).