u-observers 0.7.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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