u-observers 0.6.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee142231055e893f34d726e7bbf57b5fd27da851e49c63a5a33a3dcf6a000abe
4
- data.tar.gz: 5ca62a10c247a02435622b8190114f45c8e28be50f9a5e8e8dd5f21c42a99c2c
3
+ metadata.gz: 5098d44090f611866c96aa4005c6475742972a9a986dbbcd5618753932b33817
4
+ data.tar.gz: 8b18f175cc5ff7fd1aae8e34302be62a1f295a151231c62e253d4b984d0225b8
5
5
  SHA512:
6
- metadata.gz: c8bb160a235375a6a99c2c379413b79952ac85492211feb08dc77e0435e7e87aef3edc5c68bed81b16364a5197615321ea3a894c6bbad9143c831fb63f781fdd
7
- data.tar.gz: c861884ac244f036a9ab78edce25cf857fed41afcf18467354dd784e27728921b3a6c61695cbbf86ae3d0bef2ad71a2364e84bed2d9b6f6f33d27bae810ab0cf
6
+ metadata.gz: e51002b6af05c7aba17eff30ba962d1d726ce9e169ae19c8a71626c035daf7c1e4e6da727c74f73c06c46ad0f5b1faaad1d1248df018ada97aa123a613e1dedf
7
+ data.tar.gz: 20972cd57db02b9a25905896af9429a597cfb22e951fe73a3af6346962cd1f3574df30c241b7aa9ef3a99c502097ad0a7d75210a33227a98c6d3435371c8bb03
@@ -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,386 @@
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 been 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).
32
+
33
+ # Table of contents <!-- omit in toc -->
34
+ - [Installation](#installation)
35
+ - [Compatibility](#compatibility)
36
+ - [Usage](#usage)
37
+ - [Passing a context for your observers](#passing-a-context-for-your-observers)
38
+ - [Passing data when performing observers](#passing-data-when-performing-observers)
39
+ - [What is a `Micro::Observers::Event`?](#what-is-a-microobserversevent)
40
+ - [Passing a callable as an observer](#passing-a-callable-as-an-observer)
41
+ - [Calling the observers](#calling-the-observers)
42
+ - [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
43
+ - [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
44
+ - [notify_observers_on()](#notify_observers_on)
45
+ - [notify_observers()](#notify_observers)
46
+ - [Development](#development)
47
+ - [Contributing](#contributing)
48
+ - [License](#license)
49
+ - [Code of Conduct](#code-of-conduct)
50
+
51
+ # Installation
52
+
53
+ Add this line to your application's Gemfile and `bundle install`:
10
54
 
11
55
  ```ruby
12
- gem 'micro-observers'
56
+ gem 'u-observers'
13
57
  ```
14
58
 
15
- And then execute:
59
+ # Compatibility
16
60
 
17
- $ bundle install
61
+ | u-observers | branch | ruby | activerecord |
62
+ | ----------- | ------- | -------- | ------------- |
63
+ | 2.0.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
64
+ | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
18
65
 
19
- Or install it yourself as:
66
+ > **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
67
 
21
- $ gem install u-observers
68
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
22
69
 
23
70
  ## Usage
24
71
 
25
- TODO: Write usage instructions here
72
+ Any class with `Micro::Observers` module included can notify events to attached observers.
73
+
74
+ ```ruby
75
+ require 'securerandom'
76
+
77
+ class Order
78
+ include Micro::Observers
79
+
80
+ attr_reader :code
81
+
82
+ def initialize
83
+ @code, @status = SecureRandom.alphanumeric, :draft
84
+ end
85
+
86
+ def canceled?
87
+ @status == :canceled
88
+ end
89
+
90
+ def cancel!
91
+ return self if canceled?
92
+
93
+ @status = :canceled
94
+
95
+ observers.subject_changed!
96
+ observers.notify(:canceled) and return self
97
+ end
98
+ end
99
+
100
+ module OrderEvents
101
+ def self.canceled(order)
102
+ puts "The order #(#{order.code}) has been canceled."
103
+ end
104
+ end
105
+
106
+ order = Order.new
107
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
108
+
109
+ order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
110
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]
111
+
112
+ order.canceled?
113
+ # false
114
+
115
+ order.cancel!
116
+ # The message below will be printed by the observer (OrderEvents):
117
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
118
+
119
+ order.canceled?
120
+ # true
121
+
122
+ order.observers.detach(OrderEvents) # detaching multiple observers. e.g. observers.detach(A, B, C)
123
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]
124
+
125
+ order.canceled?
126
+ # true
127
+
128
+ order.observers.subject_changed!
129
+ order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.
130
+ ```
131
+
132
+ **Highlights of the previous example:**
133
+
134
+ To avoid an undesired behavior, do you need to mark the subject as changed before notify your observers about some event.
135
+
136
+ You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
137
+
138
+ 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)`
139
+
140
+ 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.
141
+
142
+ ```ruby
143
+ order.observers.notify
144
+ # ArgumentError (no events (expected at least 1))
145
+ ```
146
+
147
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
148
+
149
+ ### Passing a context for your observers
150
+
151
+ To pass a context (any kind of Ruby object) for one or more observers, you will need to use the `context:` keyword as the last argument of the `#attach` method.
152
+
153
+ When the observer method receives two arguments, the first one will be the subject, and the second one an instance of `Micro::Observers::Event`.
154
+
155
+ ```ruby
156
+ class Order
157
+ include Micro::Observers
158
+
159
+ def cancel!
160
+ observers.subject_changed!
161
+ observers.notify(:canceled)
162
+ self
163
+ end
164
+ end
165
+
166
+ module OrderEvents
167
+ def self.canceled(order, event)
168
+ puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
169
+ end
170
+ end
171
+
172
+ order = Order.new
173
+ order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
174
+ order.cancel!
175
+ # The message below will be printed by the observer (OrderEvents):
176
+ # The order #(70196221441820) has been canceled. (from: example #2)
177
+ ```
178
+
179
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
180
+
181
+ ### Passing data when performing observers
182
+
183
+ The [`event context`](#passing-a-context-for-your-observers) is a value that is stored when you attach your observer. But sometimes, will be useful to send some additional data when broadcasting an event to the observers.
184
+
185
+ ```ruby
186
+ class Order
187
+ include Micro::Observers
188
+ end
189
+
190
+ module OrderHandler
191
+ def self.changed(order, event)
192
+ puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
193
+ end
194
+ end
195
+
196
+ order = Order.new
197
+ order.observers.attach(OrderHandler, context: { from: 'example #3' })
198
+ order.observers.subject_changed!
199
+ order.observers.notify(:changed, data: 1)
200
+ # The message below will be printed by the observer (OrderHandler):
201
+ # The order #(70196221441820) received the number 1 from example #3.
202
+ ```
203
+
204
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
205
+
206
+ ### What is a `Micro::Observers::Event`?
207
+
208
+ The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
209
+
210
+ - `#name` will be the broadcasted event.
211
+ - `#subject` will be the observed subject.
212
+ - `#context` will be [the context data](#passing-a-context-for-your-observers) that was attached to the observer.
213
+ - `#data` will be [the value that was passed to the observers' notification](#passing-data-when-performing-observers).
214
+
215
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
216
+
217
+ ### Passing a callable as an observer
218
+
219
+ The `observers.on()` method enables you to attach callable as observers. It could receive three options:
220
+ 1. `:event` that will be notified
221
+ 2. `:call` with the callable object.
222
+ 3. `:with` (optional) it can define the value which will be used as the callable object's argument. So, if it receives a `Proc` a `Micro::Observers::Event` instance will be passed to it and the argument will be defined as the `Proc` output. But if this option wasn't be defined, the `Micro::Observers::Event` instance will be its argument.
223
+
224
+ ```ruby
225
+ class Person
226
+ include Micro::Observers
227
+
228
+ attr_reader :name
229
+
230
+ def initialize(name)
231
+ @name = name
232
+ end
233
+
234
+ def name=(new_name)
235
+ observers.subject_changed(new_name != @name)
236
+
237
+ return unless observers.subject_changed?
238
+
239
+ @name = new_name
240
+
241
+ observers.notify(:name_has_been_changed)
242
+ end
243
+ end
244
+
245
+ PrintPersonName = -> (data) do
246
+ puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
247
+ end
248
+
249
+ person = Person.new('Rodrigo')
250
+
251
+ person.observers.on(
252
+ event: :name_has_been_changed,
253
+ call: PrintPersonName,
254
+ with: -> event { {person: event.subject, number: rand} }
255
+ )
256
+
257
+ person.name = 'Serradura'
258
+ # The message below will be printed by the observer (PrintPersonName):
259
+ # Person name: Serradura, number: 0.5018509191706862
260
+ ```
261
+
262
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
263
+
264
+ ### Calling the observers
265
+
266
+ You can use a callable (a class, module, or object that responds to the call method) to be your observers.
267
+ To do this, you only need make use of the method `#call` instead of `#notify`.
268
+
269
+ ```ruby
270
+ class Order
271
+ include Micro::Observers
272
+
273
+ def cancel!
274
+ observers.subject_changed!
275
+ observers.call # in practice, this is a shortcut to observers.notify(:call)
276
+ self
277
+ end
278
+ end
279
+
280
+ OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
281
+
282
+ order = Order.new
283
+ order.observers.attach(OrderCancellation)
284
+ order.cancel!
285
+ # The message below will be printed by the observer (OrderCancellation):
286
+ # The order #(70196221441820) has been canceled.
287
+ ```
288
+
289
+ > **Note**: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.a
290
+
291
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
292
+
293
+ ### Notifying observers without marking them as changed
294
+
295
+ This feature needs to be used with caution!
296
+
297
+ If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
298
+
299
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
300
+
301
+ ### ActiveRecord and ActiveModel integrations
302
+
303
+ To make use of this feature you need to require an additional module (`require 'u-observers/for/active_record'`).
304
+
305
+ Gemfile example:
306
+ ```ruby
307
+ gem 'u-observers', require: 'u-observers/for/active_record'
308
+ ```
309
+
310
+ 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:
311
+
312
+
313
+ #### notify_observers_on()
314
+
315
+ The `notify_observers_on` allows you to pass one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
316
+
317
+ ```ruby
318
+ class Post < ActiveRecord::Base
319
+ include ::Micro::Observers::For::ActiveRecord
320
+
321
+ notify_observers_on(:after_commit) # passing multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
322
+ end
323
+
324
+ module TitlePrinter
325
+ def self.after_commit(post)
326
+ puts "Title: #{post.title}"
327
+ end
328
+ end
329
+
330
+ module TitlePrinterWithContext
331
+ def self.after_commit(post, event)
332
+ puts "Title: #{post.title} (from: #{event.context[:from]})"
333
+ end
334
+ end
335
+
336
+ Post.transaction do
337
+ post = Post.new(title: 'Hello world')
338
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
339
+ post.save
340
+ end
341
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
342
+ # Title: Hello world
343
+ # Title: Hello world (from: example #6)
344
+ ```
345
+
346
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
347
+
348
+ #### notify_observers()
349
+
350
+ The `notify_observers` allows you to pass one or more *events*, that will be used to notify after the execution of some `ActiveModel`/`ActiveRecord` callback.
351
+
352
+ ```ruby
353
+ class Post < ActiveRecord::Base
354
+ include ::Micro::Observers::For::ActiveRecord
355
+
356
+ after_commit(&notify_observers(:transaction_completed))
357
+ end
358
+
359
+ module TitlePrinter
360
+ def self.transaction_completed(post)
361
+ puts("Title: #{post.title}")
362
+ end
363
+ end
364
+
365
+ module TitlePrinterWithContext
366
+ def self.transaction_completed(post, event)
367
+ puts("Title: #{post.title} (from: #{event.ctx[:from]})")
368
+ end
369
+ end
370
+
371
+ Post.transaction do
372
+ post = Post.new(title: 'Olá mundo')
373
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
374
+ post.save
375
+ end
376
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
377
+ # Title: Olá mundo
378
+ # Title: Olá mundo (from: example #5)
379
+ ```
380
+
381
+ > **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.
382
+
383
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
26
384
 
27
385
  ## Development
28
386
 
@@ -3,31 +3,11 @@ require 'micro/observers/version'
3
3
  module Micro
4
4
  module Observers
5
5
  require 'micro/observers/utils'
6
- require 'micro/observers/events_or_actions'
7
- require 'micro/observers/manager'
8
-
9
- module ClassMethods
10
- def notify_observers!(with:)
11
- proc { |object| with.each { |evt_or_act| object.observers.notify(evt_or_act) } }
12
- end
13
-
14
- def notify_observers(*events)
15
- notify_observers!(with: EventsOrActions[events])
16
- end
17
-
18
- def call_observers(options = Utils::EMPTY_HASH)
19
- notify_observers!(with: EventsOrActions.fetch_actions(options))
20
- end
21
- end
22
-
23
- def self.included(base)
24
- base.extend(ClassMethods)
25
- base.send(:private_class_method, :notify_observers!)
26
- end
6
+ require 'micro/observers/event'
7
+ require 'micro/observers/set'
27
8
 
28
9
  def observers
29
- @observers ||= Observers::Manager.for(self)
10
+ @__observers ||= Observers::Set.for(self)
30
11
  end
31
-
32
12
  end
33
13
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+
6
+ class Event
7
+ require 'micro/observers/event/names'
8
+
9
+ attr_reader :name, :subject, :context, :data
10
+
11
+ def initialize(name, subject, context, data)
12
+ @name, @subject = name, subject
13
+ @context, @data = context, data
14
+ end
15
+
16
+ alias ctx context
17
+ alias subj subject
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+
6
+ class Event::Names
7
+ def self.[](value, default: Utils::EMPTY_ARRAY)
8
+ values = Utils.compact_array(value)
9
+
10
+ values.empty? ? default : values
11
+ end
12
+
13
+ NO_EVENTS_MSG = 'no events (expected at least 1)'.freeze
14
+
15
+ def self.fetch(value)
16
+ values = self[value]
17
+
18
+ return values unless values.empty?
19
+
20
+ raise ArgumentError, NO_EVENTS_MSG
21
+ end
22
+
23
+ private_constant :NO_EVENTS_MSG
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+ module For
6
+
7
+ module ActiveModel
8
+ module ClassMethods
9
+ def notify_observers!(events)
10
+ proc do |object|
11
+ object.observers.subject_changed!
12
+ object.observers.send(:broadcast_if_subject_changed, events)
13
+ end
14
+ end
15
+
16
+ def notify_observers(*events)
17
+ notify_observers!(Event::Names.fetch(events))
18
+ end
19
+
20
+ def notify_observers_on(*callback_methods)
21
+ Utils.compact_array(callback_methods).each do |callback_method|
22
+ self.public_send(callback_method, &notify_observers!([callback_method]))
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend(ClassMethods)
29
+ base.send(:private_class_method, :notify_observers!)
30
+ base.send(:include, ::Micro::Observers)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+ module For
6
+ require 'micro/observers/for/active_model'
7
+
8
+ module ActiveRecord
9
+ def self.included(base)
10
+ base.send(:include, ::Micro::Observers::For::ActiveModel)
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+
6
+ class Set
7
+ MapSubscriber = -> (observer, options) { [:observer, observer, options[:context]] }
8
+
9
+ MapSubscribers = -> (value) do
10
+ array = Utils.compact_array(value.kind_of?(Array) ? value : [])
11
+ array.map { |observer| MapSubscriber[observer, Utils::EMPTY_HASH] }
12
+ end
13
+
14
+ GetObserver = -> subscriber { subscriber[0] == :observer ? subscriber[1] : subscriber[2][0] }
15
+
16
+ EqualTo = -> (observer) { -> subscriber { GetObserver[subscriber] == observer } }
17
+
18
+ def self.for(subject)
19
+ new(subject)
20
+ end
21
+
22
+ def initialize(subject, subscribers: nil)
23
+ @subject = subject
24
+
25
+ @subject_changed = false
26
+
27
+ @subscribers = MapSubscribers.call(subscribers)
28
+ end
29
+
30
+ def count
31
+ @subscribers.size
32
+ end
33
+
34
+ def none?
35
+ @subscribers.empty?
36
+ end
37
+
38
+ def some?
39
+ !none?
40
+ end
41
+
42
+ def subject_changed?
43
+ @subject_changed
44
+ end
45
+
46
+ INVALID_BOOLEAN_MSG = 'expected a boolean (true, false)'.freeze
47
+
48
+ def subject_changed(state)
49
+ return @subject_changed = state if state == true || state == false
50
+
51
+ raise ArgumentError, INVALID_BOOLEAN_MSG
52
+ end
53
+
54
+ def subject_changed!
55
+ subject_changed(true)
56
+ end
57
+
58
+ def included?(observer)
59
+ @subscribers.any?(&EqualTo[observer])
60
+ end
61
+
62
+ def attach(*args)
63
+ options = args.last.is_a?(Hash) ? args.pop : Utils::EMPTY_HASH
64
+
65
+ Utils.compact_array(args).each do |observer|
66
+ @subscribers << MapSubscriber[observer, options] unless included?(observer)
67
+ end
68
+
69
+ self
70
+ end
71
+
72
+ def detach(*args)
73
+ Utils.compact_array(args).each do |observer|
74
+ @subscribers.delete_if(&EqualTo[observer])
75
+ end
76
+
77
+ self
78
+ end
79
+
80
+ def on(options = Utils::EMPTY_HASH)
81
+ event, callable, with = options[:event], options[:call], options[:with]
82
+
83
+ return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
84
+
85
+ @subscribers << [:callable, event, [callable, with]] unless included?(callable)
86
+
87
+ self
88
+ end
89
+
90
+ def notify(*events, data: nil)
91
+ broadcast_if_subject_changed(Event::Names.fetch(events), data)
92
+
93
+ self
94
+ end
95
+
96
+ def notify!(*events, data: nil)
97
+ broadcast(Event::Names.fetch(events), data)
98
+
99
+ self
100
+ end
101
+
102
+ CALL_EVENT = [:call].freeze
103
+
104
+ def call(*events, data: nil)
105
+ broadcast_if_subject_changed(Event::Names[events, default: CALL_EVENT], data)
106
+
107
+ self
108
+ end
109
+
110
+ def call!(*events, data: nil)
111
+ broadcast(Event::Names[events, default: CALL_EVENT], data)
112
+
113
+ self
114
+ end
115
+
116
+ def inspect
117
+ subs = @subscribers.empty? ? @subscribers : @subscribers.map(&GetObserver)
118
+
119
+ '<#%s @subject=%s @subject_changed=%p @subscribers=%p>' % [self.class, @subject, @subject_changed, subs]
120
+ end
121
+
122
+ private
123
+
124
+ def broadcast_if_subject_changed(events, data = nil)
125
+ return unless subject_changed?
126
+
127
+ broadcast(events, data)
128
+
129
+ subject_changed(false)
130
+ end
131
+
132
+ def broadcast(event_names, data)
133
+ return if @subscribers.empty?
134
+
135
+ event_names.each do |event_name|
136
+ @subscribers.each do |strategy, observer, context|
137
+ case strategy
138
+ when :observer then notify_observer(observer, event_name, context, data)
139
+ when :callable then notify_callable(observer, event_name, context, data)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def notify_observer(observer, event_name, context, data)
146
+ return unless observer.respond_to?(event_name)
147
+
148
+ handler = observer.is_a?(Proc) ? observer : observer.method(event_name)
149
+
150
+ return handler.call(@subject) if handler.arity == 1
151
+
152
+ handler.call(@subject, Event.new(event_name, @subject, context, data))
153
+ end
154
+
155
+ def notify_callable(expected_event_name, event_name, context, data)
156
+ return if expected_event_name != event_name
157
+
158
+ callable, with = context[0], context[1]
159
+ callable_arg =
160
+ if with && !with.is_a?(Proc)
161
+ with
162
+ else
163
+ event = Event.new(event_name, @subject, nil, data)
164
+
165
+ with.is_a?(Proc) ? with.call(event) : event
166
+ end
167
+
168
+ callable.call(callable_arg)
169
+ end
170
+
171
+ private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT
172
+ private_constant :MapSubscriber, :MapSubscribers, :GetObserver, :EqualTo
173
+ end
174
+
175
+ end
176
+ end
@@ -5,6 +5,7 @@ module Micro
5
5
 
6
6
  module Utils
7
7
  EMPTY_HASH = {}.freeze
8
+ EMPTY_ARRAY = [].freeze
8
9
 
9
10
  def self.compact_array(value)
10
11
  Array(value).flatten.tap(&:compact!)
@@ -1,5 +1,5 @@
1
1
  module Micro
2
2
  module Observers
3
- VERSION = '0.6.0'
3
+ VERSION = '2.0.0'
4
4
  end
5
5
  end
@@ -0,0 +1,2 @@
1
+ require 'micro/observers'
2
+ require 'micro/observers/for/active_model'
@@ -0,0 +1,2 @@
1
+ require 'micro/observers'
2
+ require 'micro/observers/for/active_record'
data/test.sh ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ bundle
4
+
5
+ rm Gemfile.lock
6
+
7
+ source $(dirname $0)/.travis.sh
8
+
9
+ rm Gemfile.lock
10
+
11
+ bundle
@@ -10,7 +10,6 @@ Gem::Specification.new do |spec|
10
10
  spec.description = %q{Simple and powerful implementation of the observer pattern.}
11
11
  spec.homepage = 'https://github.com/serradura/u-observers'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.2.0')
14
13
 
15
14
  spec.metadata['homepage_uri'] = spec.homepage
16
15
  spec.metadata['source_code_uri'] = 'https://github.com/serradura/u-observers'
@@ -24,4 +23,9 @@ Gem::Specification.new do |spec|
24
23
  spec.bindir = 'exe'
25
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
25
  spec.require_paths = ['lib']
26
+
27
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.2.0')
28
+
29
+ spec.add_development_dependency 'bundler'
30
+ spec.add_development_dependency 'rake', '~> 13.0'
27
31
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: u-observers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-26 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
13
41
  description: Simple and powerful implementation of the observer pattern.
14
42
  email:
15
43
  - rodrigo.serradura@gmail.com
@@ -18,6 +46,8 @@ extensions: []
18
46
  extra_rdoc_files: []
19
47
  files:
20
48
  - ".gitignore"
49
+ - ".tool-versions"
50
+ - ".travis.sh"
21
51
  - ".travis.yml"
22
52
  - CODE_OF_CONDUCT.md
23
53
  - Gemfile
@@ -27,11 +57,17 @@ files:
27
57
  - bin/console
28
58
  - bin/setup
29
59
  - lib/micro/observers.rb
30
- - lib/micro/observers/events_or_actions.rb
31
- - lib/micro/observers/manager.rb
60
+ - lib/micro/observers/event.rb
61
+ - lib/micro/observers/event/names.rb
62
+ - lib/micro/observers/for/active_model.rb
63
+ - lib/micro/observers/for/active_record.rb
64
+ - lib/micro/observers/set.rb
32
65
  - lib/micro/observers/utils.rb
33
66
  - lib/micro/observers/version.rb
34
67
  - lib/u-observers.rb
68
+ - lib/u-observers/for/active_model.rb
69
+ - lib/u-observers/for/active_record.rb
70
+ - test.sh
35
71
  - u-observers.gemspec
36
72
  homepage: https://github.com/serradura/u-observers
37
73
  licenses:
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Micro
4
- module Observers
5
-
6
- module EventsOrActions
7
- DEFAULTS = [:call]
8
-
9
- def self.[](value)
10
- values = Utils.compact_array(value)
11
-
12
- values.empty? ? DEFAULTS : values
13
- end
14
-
15
- def self.fetch_actions(hash)
16
- return self[hash.fetch(:actions) { hash.fetch(:action) }] if hash.is_a?(Hash)
17
-
18
- raise ArgumentError, 'expected a hash with the key :action or :actions'
19
- end
20
-
21
- private_constant :DEFAULTS
22
- end
23
-
24
- end
25
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Micro
4
- module Observers
5
-
6
- class Manager
7
- EqualTo = -> (observer) do
8
- -> item { item[0] == :observer && item[1] == observer }
9
- end
10
-
11
- def self.for(subject)
12
- new(subject)
13
- end
14
-
15
- def initialize(subject, list = nil)
16
- @subject = subject
17
-
18
- @list = Utils.compact_array(list.kind_of?(Array) ? list : [])
19
- end
20
-
21
- def included?(observer)
22
- @list.any?(&EqualTo[observer])
23
- end
24
-
25
- def attach(observer, options = Utils::EMPTY_HASH)
26
- if options[:allow_duplication] || !included?(observer)
27
- @list << [:observer, observer, options[:data]]
28
- end
29
-
30
- self
31
- end
32
-
33
- def on(options = Utils::EMPTY_HASH)
34
- event, callable, with = options[:event], options[:call], options[:with]
35
-
36
- return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
37
-
38
- arg = with.is_a?(Proc) ? with.call(@subject) : (arg || subject)
39
-
40
- @list << [:callable, event, [callable, arg]]
41
- end
42
-
43
- def detach(observer)
44
- @list.delete_if(&EqualTo[observer])
45
-
46
- self
47
- end
48
-
49
- def notify(*events)
50
- broadcast(EventsOrActions[events])
51
-
52
- self
53
- end
54
-
55
- def call(options = Utils::EMPTY_HASH)
56
- broadcast(EventsOrActions.fetch_actions(options))
57
-
58
- self
59
- end
60
-
61
- private
62
-
63
- def broadcast(evts_or_acts)
64
- evts_or_acts.each do |evt_or_act|
65
- @list.each do |strategy, observer, data|
66
- call!(observer, strategy, data, with: evt_or_act)
67
- end
68
- end
69
- end
70
-
71
- def call!(observer, strategy, data, with:)
72
- return data[0].call(data[1]) if strategy == :callable && observer == with
73
-
74
- if strategy == :observer && observer.respond_to?(with)
75
- handler = observer.method(with)
76
-
77
- return handler.call(@subject) if handler.arity == 1
78
-
79
- handler.call(@subject, data)
80
- end
81
- end
82
-
83
- private_constant :EqualTo
84
- end
85
-
86
- end
87
- end