u-observers 0.5.0 → 1.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: 2268ebc8924bf446ca73f3b1fbdb0cb4a8fba9f60eb01173e68fa7486747b1ed
4
- data.tar.gz: 329fad95d53450a550c9fc964fec4bb3732f5a63d684835f0ef6d766df839d15
3
+ metadata.gz: 4c90f3338948cf8db02bdb1a882e3191adfd52f106fc2b4c1acf873447abd82a
4
+ data.tar.gz: 2db659fbaee101e1af2838bdb89dc85838f0e9686d14fe14fd9782fc568ee1f6
5
5
  SHA512:
6
- metadata.gz: d3a2f1895221f171ca6499bca6284a9d349e77b9d6661b41a0b15c662d9f400af86d7d0b77e4331ca9a985b6db2d6628415be5422944cb09aba32d676aa537eb
7
- data.tar.gz: 82daa067cd2cf85c3b00c0aca546131e38c1ee365336d5f01cb868b81d7b91be806ecb606c61ad5e56d2364f9341f8636d093f02d6b6f424ddccc0702b11f7e7
6
+ metadata.gz: 674b8dabdc6cfca0a982c1f72582bf1fcaab34ead0f0de29d4975cc62e164c13bbded5e6118152951584fb5855b6c539510ca393b36f9cf56622bf663d7b22e2
7
+ data.tar.gz: ff1e33f5029a40745172c7196d7e21552e8f4d534c71f057bf5643e3262fe8fa243fb55219eed352195a09abc51c337476f9f8bfcd5bce5f61cee416c2ec8de7
@@ -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,288 @@
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
+ - [Calling the observers](#calling-the-observers)
39
+ - [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
40
+ - [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
41
+ - [notify_observers_on()](#notify_observers_on)
42
+ - [notify_observers()](#notify_observers)
43
+ - [Development](#development)
44
+ - [Contributing](#contributing)
45
+ - [License](#license)
46
+ - [Code of Conduct](#code-of-conduct)
47
+
48
+ # Installation
49
+
50
+ Add this line to your application's Gemfile and `bundle install`:
10
51
 
11
52
  ```ruby
12
- gem 'micro-observers'
53
+ gem 'u-observers'
13
54
  ```
14
55
 
15
- And then execute:
56
+ # Compatibility
16
57
 
17
- $ bundle install
58
+ | u-observers | branch | ruby | activerecord |
59
+ | ----------- | ------- | -------- | ------------- |
60
+ | 1.0.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
18
61
 
19
- Or install it yourself as:
62
+ > **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
63
 
21
- $ gem install u-observers
64
+ [⬆️ Back to Top](#table-of-contents-)
22
65
 
23
66
  ## Usage
24
67
 
25
- TODO: Write usage instructions here
68
+ Any class with `Micro::Observers` module included can notify events to attached observers.
69
+
70
+ ```ruby
71
+ require 'securerandom'
72
+
73
+ class Order
74
+ include Micro::Observers
75
+
76
+ attr_reader :code
77
+
78
+ def initialize
79
+ @code, @status = SecureRandom.alphanumeric, :draft
80
+ end
81
+
82
+ def canceled?
83
+ @status == :canceled
84
+ end
85
+
86
+ def cancel!
87
+ return self if canceled?
88
+
89
+ @status = :canceled
90
+
91
+ observers.subject_changed!
92
+ observers.notify(:canceled) and return self
93
+ end
94
+ end
95
+
96
+ module OrderEvents
97
+ def self.canceled(order)
98
+ puts "The order #(#{order.code}) has been canceled."
99
+ end
100
+ end
101
+
102
+ order = Order.new
103
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
104
+
105
+ order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
106
+ # <#Micro::Observers::Manager @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]
107
+
108
+ order.canceled?
109
+ # false
110
+
111
+ order.cancel!
112
+ # The message below will be printed by the observer (OrderEvents):
113
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
114
+
115
+ order.canceled?
116
+ # true
117
+ ```
118
+
119
+ **Highlights of the previous example:**
120
+
121
+ To avoid an undesired behavior, do you need to mark the subject as changed before notify your observers about some event.
122
+
123
+ You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
124
+
125
+ 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)`
126
+
127
+ 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.
128
+
129
+ ```ruby
130
+ order.observers.notify
131
+ # ArgumentError (no events (expected at least 1))
132
+ ```
133
+
134
+ [⬆️ Back to Top](#table-of-contents-)
135
+
136
+ ### Passing a context for your observers
137
+
138
+ 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.
139
+
140
+ ```ruby
141
+ class Order
142
+ include Micro::Observers
143
+
144
+ def cancel!
145
+ observers.subject_changed!
146
+ observers.notify(:canceled)
147
+ self
148
+ end
149
+ end
150
+
151
+ module OrderEvents
152
+ def self.canceled(order, context)
153
+ puts "The order #(#{order.code}) has been canceled. (from: #{context[:from]})"
154
+ end
155
+ end
156
+
157
+ order = Order.new
158
+ order.observers.attach(OrderEvents, context: { from: 'example #2' ) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
159
+ order.cancel!
160
+ # The message below will be printed by the observer (OrderEvents):
161
+ # The order #(70196221441820) has been canceled. (from: example #2)
162
+ ```
163
+
164
+ [⬆️ Back to Top](#table-of-contents-)
165
+
166
+ ### Calling the observers
167
+
168
+ You can use a callable (a class, module, or object that responds to the call method) to be your observers.
169
+ To do this, you only need make use of the method `#call` instead of `#notify`.
170
+
171
+ ```ruby
172
+ class Order
173
+ include Micro::Observers
174
+
175
+ def cancel!
176
+ observers.subject_changed!
177
+ observers.call # in practice, this is a shortcut to observers.notify(:call)
178
+ self
179
+ end
180
+ end
181
+
182
+ OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
183
+
184
+ order = Order.new
185
+ order.observers.attach(OrderCancellation)
186
+ order.cancel!
187
+ # The message below will be printed by the observer (OrderEvents):
188
+ # The order #(70196221441820) has been canceled.
189
+ ```
190
+
191
+ PS: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.a
192
+
193
+ [⬆️ Back to Top](#table-of-contents-)
194
+
195
+ ### Notifying observers without marking them as changed
196
+
197
+ This feature needs to be used with caution!
198
+
199
+ If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
200
+
201
+ [⬆️ Back to Top](#table-of-contents-)
202
+
203
+ ### ActiveRecord and ActiveModel integrations
204
+
205
+ To make use of this feature you need to require an additional module (`require 'u-observers/for/active_record'`).
206
+
207
+ Gemfile example:
208
+ ```ruby
209
+ gem 'u-observers', require: 'u-observers/for/active_record'
210
+ ```
211
+
212
+ 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:
213
+
214
+
215
+ #### notify_observers_on()
216
+
217
+ The `notify_observers_on` allows you to pass one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
218
+
219
+ ```ruby
220
+ class Post < ActiveRecord::Base
221
+ include ::Micro::Observers::For::ActiveRecord
222
+
223
+ notify_observers_on(:after_commit) # passing multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
224
+ end
225
+
226
+ module TitlePrinter
227
+ def self.after_commit(post)
228
+ puts "Title: #{post.title}"
229
+ end
230
+ end
231
+
232
+ module TitlePrinterWithContext
233
+ def self.after_commit(post, context)
234
+ puts "Title: #{post.title} (from: #{context[:from]})"
235
+ end
236
+ end
237
+
238
+ Post.transaction do
239
+ post = Post.new(title: 'Hello world')
240
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example 4' })
241
+ post.save
242
+ end
243
+ # The message below will be printed by the observer (OrderEvents):
244
+ # Title: Hello world
245
+ # Title: Hello world (from: example 4)
246
+ ```
247
+
248
+ [⬆️ Back to Top](#table-of-contents-)
249
+
250
+ #### notify_observers()
251
+
252
+ 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.
253
+
254
+ ```ruby
255
+ class Post < ActiveRecord::Base
256
+ include ::Micro::Observers::For::ActiveRecord
257
+
258
+ after_commit(&notify_observers(:transaction_completed))
259
+ end
260
+
261
+ module TitlePrinter
262
+ def self.transaction_completed(post)
263
+ puts("Title: #{post.title}")
264
+ end
265
+ end
266
+
267
+ module TitlePrinterWithContext
268
+ def self.transaction_completed(post, context)
269
+ puts("Title: #{post.title} (from: #{context[:from]})")
270
+ end
271
+ end
272
+
273
+ Post.transaction do
274
+ post = Post.new(title: 'Olá mundo')
275
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example 5' })
276
+ post.save
277
+ end
278
+ # The message below will be printed by the observer (OrderEvents):
279
+ # Title: Olá mundo
280
+ # Title: Olá mundo (from: example 5)
281
+ ```
282
+
283
+ PS: You can use `include ::Micro::Observers::For::ActiveModel` if your class only makes use of the `ActiveModel` and all the previous examples will work.
284
+
285
+ [⬆️ Back to Top](#table-of-contents-)
26
286
 
27
287
  ## Development
28
288
 
@@ -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'
6
+ require 'micro/observers/events'
7
7
  require 'micro/observers/manager'
8
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
27
-
28
9
  def observers
29
- @observers ||= Observers::Manager.for(self)
10
+ @__observers ||= Observers::Manager.for(self)
30
11
  end
31
-
32
12
  end
33
13
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Observers
5
+
6
+ module Events
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!(Events.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
@@ -4,27 +4,78 @@ module Micro
4
4
  module Observers
5
5
 
6
6
  class Manager
7
- EqualTo = -> (observer) do
8
- -> item { item[0] == :observer && item[1] == observer }
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] }
9
12
  end
10
13
 
14
+ GetObserver = -> subscriber { subscriber[0] == :observer ? subscriber[1] : subscriber[2][0] }
15
+
16
+ EqualTo = -> (observer) { -> subscriber { GetObserver[subscriber] == observer } }
17
+
11
18
  def self.for(subject)
12
19
  new(subject)
13
20
  end
14
21
 
15
- def initialize(subject, list = nil)
22
+ def initialize(subject, subscribers: nil)
16
23
  @subject = subject
17
24
 
18
- @list = Utils.compact_array(list.kind_of?(Array) ? list : [])
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
+ if state == true || state == false
50
+ @subject_changed = state
51
+
52
+ return self
53
+ end
54
+
55
+ raise ArgumentError, INVALID_BOOLEAN_MSG
56
+ end
57
+
58
+ def subject_changed!
59
+ subject_changed(true)
19
60
  end
20
61
 
21
62
  def included?(observer)
22
- @list.any?(&EqualTo[observer])
63
+ @subscribers.any?(&EqualTo[observer])
23
64
  end
24
65
 
25
- def attach(observer, options = Utils::EMPTY_HASH)
26
- if options[:allow_duplication] || !included?(observer)
27
- @list << [:observer, observer, options[:data]]
66
+ def attach(*args)
67
+ options = args.last.is_a?(Hash) ? args.pop : Utils::EMPTY_HASH
68
+
69
+ Utils.compact_array(args).each do |observer|
70
+ @subscribers << MapSubscriber[observer, options] unless included?(observer)
71
+ end
72
+
73
+ self
74
+ end
75
+
76
+ def detach(*args)
77
+ Utils.compact_array(args).each do |observer|
78
+ @subscribers.delete_if(&EqualTo[observer])
28
79
  end
29
80
 
30
81
  self
@@ -35,50 +86,86 @@ module Micro
35
86
 
36
87
  return self unless event.is_a?(Symbol) && callable.respond_to?(:call)
37
88
 
38
- arg = with.is_a?(Proc) ? with.call(@subject) : (arg || subject)
89
+ @subscribers << [:callable, event, [callable, with]] unless included?(callable)
39
90
 
40
- @list << [:callable, event, [callable, arg]]
91
+ self
41
92
  end
42
93
 
43
- def detach(observer)
44
- @list.delete_if(&EqualTo[observer])
94
+ def notify(*events)
95
+ broadcast_if_subject_changed(Events.fetch(events))
45
96
 
46
97
  self
47
98
  end
48
99
 
49
- def notify(*events)
50
- EventsOrActions[events].each { |act| notify!(act) }
100
+ def notify!(*events)
101
+ broadcast(Events.fetch(events))
102
+
103
+ self
104
+ end
105
+
106
+ CALL_EVENT = [:call].freeze
107
+
108
+ def call(*events)
109
+ broadcast_if_subject_changed(Events[events, default: CALL_EVENT])
51
110
 
52
111
  self
53
112
  end
54
113
 
55
- def call(options = Utils::EMPTY_HASH)
56
- EventsOrActions.fetch_actions(options).each { |act| notify!(act) }
114
+ def call!(*events)
115
+ broadcast(Events[events, default: CALL_EVENT])
57
116
 
58
117
  self
59
118
  end
60
119
 
120
+ def inspect
121
+ subs = @subscribers.empty? ? @subscribers : @subscribers.map(&GetObserver)
122
+
123
+ '<#%s @subject=%s @subject_changed=%p @subscribers=%p>' % [self.class, @subject, @subject_changed, subs]
124
+ end
125
+
61
126
  private
62
127
 
63
- def notify!(evt_or_act)
64
- @list.each do |strategy, observer, data|
65
- call!(observer, strategy, data, with: evt_or_act)
128
+ def broadcast_if_subject_changed(events)
129
+ return unless subject_changed?
130
+
131
+ broadcast(events)
132
+
133
+ subject_changed(false)
134
+ end
135
+
136
+ def broadcast(events)
137
+ return if @subscribers.empty?
138
+
139
+ events.each do |event|
140
+ @subscribers.each do |strategy, observer, context|
141
+ case strategy
142
+ when :observer then notify_observer(observer, event, context)
143
+ when :callable then notify_callable(observer, event, context)
144
+ end
145
+ end
66
146
  end
67
147
  end
68
148
 
69
- def call!(observer, strategy, data, with:)
70
- return data[0].call(data[1]) if strategy == :callable && observer == with
149
+ def notify_observer(observer, method_name, context)
150
+ return unless observer.respond_to?(method_name)
71
151
 
72
- if strategy == :observer && observer.respond_to?(with)
73
- handler = observer.method(with)
152
+ handler = observer.is_a?(Proc) ? observer : observer.method(method_name)
74
153
 
75
- return handler.call(@subject) if handler.arity == 1
154
+ handler.arity == 1 ? handler.call(@subject) : handler.call(@subject, context)
155
+ end
76
156
 
77
- handler.call(@subject, data)
78
- end
157
+ def notify_callable(expected_event, event, context)
158
+ return if expected_event != event
159
+
160
+ callable, with = context[0], context[1]
161
+
162
+ arg = with.is_a?(Proc) ? with.call(@subject) : (with || @subject)
163
+
164
+ callable.call(arg)
79
165
  end
80
166
 
81
- private_constant :EqualTo
167
+ private_constant :INVALID_BOOLEAN_MSG, :CALL_EVENT
168
+ private_constant :MapSubscriber, :MapSubscribers, :GetObserver, :EqualTo
82
169
  end
83
170
 
84
171
  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.5.0'
3
+ VERSION = '1.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.5.0
4
+ version: 1.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-05 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,16 @@ files:
27
57
  - bin/console
28
58
  - bin/setup
29
59
  - lib/micro/observers.rb
30
- - lib/micro/observers/events_or_actions.rb
60
+ - lib/micro/observers/events.rb
61
+ - lib/micro/observers/for/active_model.rb
62
+ - lib/micro/observers/for/active_record.rb
31
63
  - lib/micro/observers/manager.rb
32
64
  - lib/micro/observers/utils.rb
33
65
  - lib/micro/observers/version.rb
34
66
  - lib/u-observers.rb
67
+ - lib/u-observers/for/active_model.rb
68
+ - lib/u-observers/for/active_record.rb
69
+ - test.sh
35
70
  - u-observers.gemspec
36
71
  homepage: https://github.com/serradura/u-observers
37
72
  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