u-observers 0.7.0 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20e65a3bddc28ca3121413e665f084f096c7b266304d9c486bc2d7c9b618ab8a
4
- data.tar.gz: 455304b5999249cd475f5df8a44e0d855333ccf208f41d9cd98c7cfe1bba3cac
3
+ metadata.gz: 52f8cf4c9f76af4b9438412dc60fa7b4156fa84afec92f89a12bb40b90d1ed32
4
+ data.tar.gz: 1c1ad202a47f467e468186b58671bed796656180660302f057005a7387bdcc7f
5
5
  SHA512:
6
- metadata.gz: 9fe33fb9bc6bcb060f32329e28774abb6a7b546e178e531d045043889db64c403afd00171351e733df33cc812e84763c9aaf47f89c82dc6cd37264ab8ba8fd04
7
- data.tar.gz: af195ddbdacb7e811f8c30d45d389d4b9472df7d06966009740e8cf4400676add7d80cda18db166795fafe27ae82277ed1930a2c72053bf427f4fefe424c37bc
6
+ metadata.gz: c2f779070c6e11ef08c71b85f9d90342907cef3e910e7f6a7b61bbebeeec283baf56d197df1edf3bb4bb7437424e43d74208d97fed1e745b76367b2c14fb2206
7
+ data.tar.gz: 7995b96fe8868e126fd053e05e831b92adc421325b133324222591c74a288ba4de9677dc59b2b5f57a8d8a437c04a342f04a9548ed460546e9f56b571e4ac3c5
@@ -0,0 +1 @@
1
+ ruby 2.6.5
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+
3
+ ruby_v=$(ruby -v)
4
+
5
+ bundle update
6
+ bundle exec rake test
7
+
8
+ ACTIVERECORD_VERSION='3.2' bundle update
9
+ ACTIVERECORD_VERSION='3.2' bundle exec rake test
10
+
11
+ ACTIVERECORD_VERSION='4.0' bundle update
12
+ ACTIVERECORD_VERSION='4.0' bundle exec rake test
13
+
14
+ ACTIVERECORD_VERSION='4.1' bundle update
15
+ ACTIVERECORD_VERSION='4.1' bundle exec rake test
16
+
17
+ ACTIVERECORD_VERSION='4.2' bundle update
18
+ ACTIVERECORD_VERSION='4.2' bundle exec rake test
19
+
20
+ ACTIVERECORD_VERSION='5.0' bundle update
21
+ ACTIVERECORD_VERSION='5.0' bundle exec rake test
22
+
23
+ ACTIVERECORD_VERSION='5.1' bundle update
24
+ ACTIVERECORD_VERSION='5.1' bundle exec rake test
25
+
26
+ if [[ ! $ruby_v =~ '2.2.0' ]]; then
27
+ ACTIVERECORD_VERSION='5.2' bundle update
28
+ ACTIVERECORD_VERSION='5.2' bundle exec rake test
29
+ fi
30
+
31
+ if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
32
+ ACTIVERECORD_VERSION='6.0' bundle update
33
+ ACTIVERECORD_VERSION='6.0' bundle exec rake test
34
+ fi
@@ -1,6 +1,30 @@
1
1
  ---
2
2
  language: ruby
3
- cache: bundler
3
+
4
+ sudo: false
5
+
4
6
  rvm:
5
- - 2.6.5
6
- before_install: gem install bundler -v 2.1.4
7
+ - 2.2.0
8
+ - 2.3.0
9
+ - 2.4.0
10
+ - 2.5.0
11
+ - 2.6.0
12
+ - 2.7.0
13
+
14
+ cache: bundler
15
+
16
+ before_install:
17
+ - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
18
+ - gem install bundler -v '< 2'
19
+
20
+ install: bundle install --jobs=3 --retry=3
21
+
22
+ before_script:
23
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
24
+ - chmod +x ./cc-test-reporter
25
+ - "./cc-test-reporter before-build"
26
+
27
+ script: "./.travis.sh"
28
+
29
+ after_success:
30
+ - "./cc-test-reporter after-build -t simplecov"
data/Gemfile CHANGED
@@ -3,12 +3,38 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in u-observers.gemspec
4
4
  gemspec
5
5
 
6
- gem 'rake', '~> 12.0'
7
- gem 'minitest', '~> 5.0'
6
+ activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '6.1')
7
+
8
+ activerecord = case activerecord_version
9
+ when '3.2' then '3.2.22'
10
+ when '4.0' then '4.0.13'
11
+ when '4.1' then '4.1.16'
12
+ when '4.2' then '4.2.11'
13
+ when '5.0' then '5.0.7'
14
+ when '5.1' then '5.1.7'
15
+ when '5.2' then '5.2.3'
16
+ when '6.0' then '6.0.3'
17
+ end
18
+
19
+ simplecov_version =
20
+ case RUBY_VERSION
21
+ when /\A2.[23]/ then '~> 0.17.1'
22
+ when /\A2.4/ then '~> 0.18.5'
23
+ else '~> 0.19'
24
+ end
8
25
 
9
26
  group :test do
10
- gem 'activerecord', require: 'active_record'
11
- gem 'sqlite3'
27
+ gem 'minitest', activerecord_version < '4.1' ? '~> 4.2' : '~> 5.0'
28
+ gem 'simplecov', simplecov_version, require: false
29
+
30
+ if activerecord
31
+ sqlite3 =
32
+ case activerecord
33
+ when /\A6\.0/, nil then '~> 1.4.0'
34
+ else '~> 1.3.0'
35
+ end
12
36
 
13
- gem 'simplecov', '~> 0.19', require: false
37
+ gem 'sqlite3', sqlite3
38
+ gem 'activerecord', activerecord, require: 'active_record'
39
+ end
14
40
  end
data/README.md CHANGED
@@ -1,28 +1,409 @@
1
- # Micro::Observers
1
+ <p align="center">
2
+ <h1 align="center">👀 μ-observers</h1>
3
+ <p align="center"><i>Simple and powerful implementation of the observer pattern.</i></p>
4
+ <br>
5
+ </p>
2
6
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/micro/observers`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
4
9
 
5
- TODO: Delete this and the text above, and describe your gem
10
+ <a href="https://rubygems.org/gems/u-observers">
11
+ <img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
12
+ </a>
6
13
 
7
- ## Installation
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>
8
17
 
9
- Add this line to your application's Gemfile:
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">
24
+ </a>
25
+ </p>
26
+
27
+ 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
+
29
+ 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
+
31
+ 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
+
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).
34
+
35
+ # Table of contents <!-- omit in toc -->
36
+ - [Installation](#installation)
37
+ - [Compatibility](#compatibility)
38
+ - [Usage](#usage)
39
+ - [Sharing a context with your observers](#sharing-a-context-with-your-observers)
40
+ - [Sharing data when notifying the observers](#sharing-data-when-notifying-the-observers)
41
+ - [What is a `Micro::Observers::Event`?](#what-is-a-microobserversevent)
42
+ - [Using a callable as an observer](#using-a-callable-as-an-observer)
43
+ - [Calling the observers](#calling-the-observers)
44
+ - [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
45
+ - [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
46
+ - [notify_observers_on()](#notify_observers_on)
47
+ - [notify_observers()](#notify_observers)
48
+ - [Development](#development)
49
+ - [Contributing](#contributing)
50
+ - [License](#license)
51
+ - [Code of Conduct](#code-of-conduct)
52
+
53
+ # Installation
54
+
55
+ Add this line to your application's Gemfile and `bundle install`:
10
56
 
11
57
  ```ruby
12
- gem 'micro-observers'
58
+ gem 'u-observers'
13
59
  ```
14
60
 
15
- And then execute:
61
+ # Compatibility
16
62
 
17
- $ bundle install
63
+ | u-observers | branch | ruby | activerecord |
64
+ | ----------- | ------- | -------- | ------------- |
65
+ | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
66
+ | 2.1.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
67
+ | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
18
68
 
19
- Or install it yourself as:
69
+ > **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).
20
70
 
21
- $ gem install u-observers
71
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
22
72
 
23
73
  ## Usage
24
74
 
25
- TODO: Write usage instructions here
75
+ Any class with `Micro::Observers` module included can notify events to attached observers.
76
+
77
+ ```ruby
78
+ require 'securerandom'
79
+
80
+ class Order
81
+ include Micro::Observers
82
+
83
+ attr_reader :code
84
+
85
+ def initialize
86
+ @code, @status = SecureRandom.alphanumeric, :draft
87
+ end
88
+
89
+ def canceled?
90
+ @status == :canceled
91
+ end
92
+
93
+ def cancel!
94
+ return self if canceled?
95
+
96
+ @status = :canceled
97
+
98
+ observers.subject_changed!
99
+ observers.notify(:canceled) and return self
100
+ end
101
+ end
102
+
103
+ module OrderEvents
104
+ def self.canceled(order)
105
+ puts "The order #(#{order.code}) has been canceled."
106
+ end
107
+ end
108
+
109
+ order = Order.new
110
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
111
+
112
+ order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
113
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
114
+
115
+ order.canceled?
116
+ # false
117
+
118
+ order.cancel!
119
+ # The message below will be printed by the observer (OrderEvents):
120
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
121
+
122
+ order.canceled?
123
+ # true
124
+
125
+ order.observers.detach(OrderEvents) # detaching multiple observers. e.g. observers.detach(A, B, C)
126
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>
127
+
128
+ order.canceled?
129
+ # true
130
+
131
+ order.observers.subject_changed!
132
+ order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.
133
+ ```
134
+
135
+ **Highlights of the previous example:**
136
+
137
+ To avoid an undesired behavior, you need to mark the subject as changed before notifying your observers about some event.
138
+
139
+ You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
140
+
141
+ But if you need to apply some conditional to mark a change, you can use the `#subject_changed` method. e.g. `observers.subject_changed(name != new_name)`
142
+
143
+ The `#notify` method always requires an event to make a broadcast. So, if you try to use it without one or more events (symbol values) you will get an exception.
144
+
145
+ ```ruby
146
+ order.observers.notify
147
+ # ArgumentError (no events (expected at least 1))
148
+ ```
149
+
150
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
151
+
152
+ ### Sharing a context with your observers
153
+
154
+ To share a context value (any kind of Ruby object) with one or more observers, you will need to use the `:context` keyword as the last argument of the `#attach` method. This feature gives you a unique opportunity to share a value in the attaching moment.
155
+
156
+ When the observer method receives two arguments, the first one will be the subject, and the second one an instance of `Micro::Observers::Event` that will have the given context value.
157
+
158
+ ```ruby
159
+ class Order
160
+ include Micro::Observers
161
+
162
+ def cancel!
163
+ observers.subject_changed!
164
+ observers.notify(:canceled)
165
+ self
166
+ end
167
+ end
168
+
169
+ module OrderEvents
170
+ def self.canceled(order, event)
171
+ puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
172
+ end
173
+ end
174
+
175
+ order = Order.new
176
+ order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
177
+ order.cancel!
178
+ # The message below will be printed by the observer (OrderEvents):
179
+ # The order #(70196221441820) has been canceled. (from: example #2)
180
+ ```
181
+
182
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
183
+
184
+ ### Sharing data when notifying the observers
185
+
186
+ As previously mentioned, the [`event context`](#sharing-a-context-with-your-observers) is a value that is stored when you attach your observer. But sometimes, it will be useful to send some additional data when broadcasting an event to the observers. The `event data` gives you this unique opportunity to share some value at the the notification moment.
187
+
188
+ ```ruby
189
+ class Order
190
+ include Micro::Observers
191
+ end
192
+
193
+ module OrderHandler
194
+ def self.changed(order, event)
195
+ puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
196
+ end
197
+ end
198
+
199
+ order = Order.new
200
+ order.observers.attach(OrderHandler, context: { from: 'example #3' })
201
+ order.observers.subject_changed!
202
+ order.observers.notify(:changed, data: 1)
203
+ # The message below will be printed by the observer (OrderHandler):
204
+ # The order #(70196221441820) received the number 1 from example #3.
205
+ ```
206
+
207
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
208
+
209
+ ### What is a `Micro::Observers::Event`?
210
+
211
+ The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
212
+
213
+ - `#name` will be the broadcasted event.
214
+ - `#subject` will be the observed subject.
215
+ - `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined in the moment that you attach the observer.
216
+ - `#data` will be [the value that was shared in the observers' notification](#sharing-data-when-notifying-the-observers).
217
+ - `#ctx` is an alias for the `#context` method.
218
+ - `#subj` is an alias for the `#subject` method.
219
+
220
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
221
+
222
+ ### Using a callable as an observer
223
+
224
+ The `observers.on()` method enables you to attach a callable as an observer.
225
+
226
+ 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).
227
+
228
+ This method receives the below options:
229
+ 1. `:event` the expected event name.
230
+ 2. `:call` the callable object itself.
231
+ 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.
232
+ 4. `:context` will be the context data that was defined in the moment that you attach the observer.
233
+
234
+ ```ruby
235
+ class Person
236
+ include Micro::Observers
237
+
238
+ attr_reader :name
239
+
240
+ def initialize(name)
241
+ @name = name
242
+ end
243
+
244
+ def name=(new_name)
245
+ return unless observers.subject_changed(new_name != @name)
246
+
247
+ @name = new_name
248
+
249
+ observers.notify(:name_has_been_changed)
250
+ end
251
+ end
252
+
253
+ PrintPersonName = -> (data) do
254
+ puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
255
+ end
256
+
257
+ person = Person.new('Rodrigo')
258
+
259
+ person.observers.on(
260
+ event: :name_has_been_changed,
261
+ call: PrintPersonName,
262
+ with: -> event { {person: event.subject, number: event.context} },
263
+ context: rand
264
+ )
265
+
266
+ person.name = 'Serradura'
267
+ # The message below will be printed by the observer (PrintPersonName):
268
+ # Person name: Serradura, number: 0.5018509191706862
269
+ ```
270
+
271
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
272
+
273
+ ### Calling the observers
274
+
275
+ You can use a callable (a class, module, or object that responds to the call method) to be your observers.
276
+ To do this, you only need to make use of the method `#call` instead of `#notify`.
277
+
278
+ ```ruby
279
+ class Order
280
+ include Micro::Observers
281
+
282
+ def cancel!
283
+ observers.subject_changed!
284
+ observers.call # in practice, this is a shortcut to observers.notify(:call)
285
+ self
286
+ end
287
+ end
288
+
289
+ OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
290
+
291
+ order = Order.new
292
+ order.observers.attach(OrderCancellation)
293
+ order.cancel!
294
+ # The message below will be printed by the observer (OrderCancellation):
295
+ # The order #(70196221441820) has been canceled.
296
+ ```
297
+
298
+ > **Note**: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.
299
+
300
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
301
+
302
+ ### Notifying observers without marking them as changed
303
+
304
+ This feature needs to be used with caution!
305
+
306
+ If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
307
+
308
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
309
+
310
+ ### ActiveRecord and ActiveModel integrations
311
+
312
+ To make use of this feature you need to require an additional module.
313
+
314
+ Gemfile example:
315
+ ```ruby
316
+ gem 'u-observers', require: 'u-observers/for/active_record'
317
+ ```
318
+
319
+ 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:
320
+
321
+
322
+ #### notify_observers_on()
323
+
324
+ The `notify_observers_on` allows you to define one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
325
+
326
+ ```ruby
327
+ class Post < ActiveRecord::Base
328
+ include ::Micro::Observers::For::ActiveRecord
329
+
330
+ notify_observers_on(:after_commit) # using multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
331
+
332
+ # The method above does the same as the commented example below.
333
+ #
334
+ # after_commit do |record|
335
+ # record.subject_changed!
336
+ # record.notify(:after_commit)
337
+ # end
338
+ end
339
+
340
+ module TitlePrinter
341
+ def self.after_commit(post)
342
+ puts "Title: #{post.title}"
343
+ end
344
+ end
345
+
346
+ module TitlePrinterWithContext
347
+ def self.after_commit(post, event)
348
+ puts "Title: #{post.title} (from: #{event.context[:from]})"
349
+ end
350
+ end
351
+
352
+ Post.transaction do
353
+ post = Post.new(title: 'Hello world')
354
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
355
+ post.save
356
+ end
357
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
358
+ # Title: Hello world
359
+ # Title: Hello world (from: example #6)
360
+ ```
361
+
362
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
363
+
364
+ #### notify_observers()
365
+
366
+ 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.
367
+
368
+ ```ruby
369
+ class Post < ActiveRecord::Base
370
+ include ::Micro::Observers::For::ActiveRecord
371
+
372
+ after_commit(&notify_observers(:transaction_completed))
373
+
374
+ # The method above does the same as the commented example below.
375
+ #
376
+ # after_commit do |record|
377
+ # record.subject_changed!
378
+ # record.notify(:transaction_completed)
379
+ # end
380
+ end
381
+
382
+ module TitlePrinter
383
+ def self.transaction_completed(post)
384
+ puts("Title: #{post.title}")
385
+ end
386
+ end
387
+
388
+ module TitlePrinterWithContext
389
+ def self.transaction_completed(post, event)
390
+ puts("Title: #{post.title} (from: #{event.ctx[:from]})")
391
+ end
392
+ end
393
+
394
+ Post.transaction do
395
+ post = Post.new(title: 'Olá mundo')
396
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
397
+ post.save
398
+ end
399
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
400
+ # Title: Olá mundo
401
+ # Title: Olá mundo (from: example #5)
402
+ ```
403
+
404
+ > **Note**: You can use `include ::Micro::Observers::For::ActiveModel` if your class only makes use of the `ActiveModel` and all the previous examples will work.
405
+
406
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
26
407
 
27
408
  ## Development
28
409