u-observers 0.6.0 → 2.0.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: 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