u-observers 0.9.0 → 2.2.1

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: bfa6d5600e86dd6086b2d6ebd4cd1b015ebe3331bd4cd61be90f2d96a1abf601
4
- data.tar.gz: b483b0721def3d41085260343edf1041905b376264354b6ffbd07cbb5386725f
3
+ metadata.gz: d7ebc064f3e8384d9322c640d3d52b1bf70bf6d5159ae67bf3cd34bdf67482eb
4
+ data.tar.gz: 398d1ab0fc5852a42416c5bc16c377c4a55d760e1e327e2149b58b7db23cae73
5
5
  SHA512:
6
- metadata.gz: '06318ee21d6b9e842fa7ad752f9ccb137286667211fc43bed267d5593506d239fe946f03159236e00e0549c45e71d5de1f36cb1213ce698c3ad3b8edaf4c5abf'
7
- data.tar.gz: 91aac7ae90a990f5575838bf13ddfd174066257a2b82620e82fc8af963519d9c85526aa742005b3d7c1cc78c1dba67d557f62ab233d34b30b70b8c69a945a929
6
+ metadata.gz: 0d805d69fb5cb7d08d1ef5aec34742d08845d2d544cec1262ec63d99788adb5053ed87558957849f1b9843b772929e9490f1ae243ee3c568d87b94e1b432a8c5
7
+ data.tar.gz: 932bfa8189f162fea8160779d521168fdebe253d36414c0b05fbccf2a8c661dc94e127fb5a942523e0ae415142d6090cd8a361124f60f550b7d2166c87106c0e
data/.travis.sh CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ruby_v=$(ruby -v)
4
4
 
5
+ bundle update
6
+ bundle exec rake test
7
+
5
8
  ACTIVERECORD_VERSION='3.2' bundle update
6
9
  ACTIVERECORD_VERSION='3.2' bundle exec rake test
7
10
 
data/Gemfile CHANGED
@@ -23,16 +23,18 @@ simplecov_version =
23
23
  else '~> 0.19'
24
24
  end
25
25
 
26
- sqlite3 =
27
- case activerecord
28
- when /\A6\.0/, nil then '~> 1.4.0'
29
- else '~> 1.3.0'
30
- end
31
-
32
26
  group :test do
33
27
  gem 'minitest', activerecord_version < '4.1' ? '~> 4.2' : '~> 5.0'
34
28
  gem 'simplecov', simplecov_version, require: false
35
29
 
36
- gem 'sqlite3', sqlite3
37
- gem 'activerecord', activerecord, require: 'active_record'
30
+ if activerecord
31
+ sqlite3 =
32
+ case activerecord
33
+ when /\A6\.0/, nil then '~> 1.4.0'
34
+ else '~> 1.3.0'
35
+ end
36
+
37
+ gem 'sqlite3', sqlite3
38
+ gem 'activerecord', activerecord, require: 'active_record'
39
+ end
38
40
  end
data/README.md CHANGED
@@ -1,28 +1,512 @@
1
- # Micro::Observers
1
+ <p align="center">
2
+ <h1 align="center">👀 μ-observers</h1>
3
+ <p align="center"><i>Simple and powerful implementation of the observer pattern.</i></p>
4
+ <br>
5
+ </p>
2
6
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/micro/observers`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
4
9
 
5
- TODO: Delete this and the text above, and describe your gem
10
+ <a href="https://rubygems.org/gems/u-observers">
11
+ <img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
12
+ </a>
6
13
 
7
- ## Installation
14
+ <a href="https://travis-ci.com/serradura/u-observers">
15
+ <img alt="Build Status" src="https://travis-ci.com/serradura/u-observers.svg?branch=main">
16
+ </a>
8
17
 
9
- Add this line to your application's Gemfile:
18
+ <a href="https://codeclimate.com/github/serradura/u-observers/maintainability">
19
+ <img alt="Maintainability" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/maintainability">
20
+ </a>
21
+
22
+ <a href="https://codeclimate.com/github/serradura/u-observers/test_coverage">
23
+ <img alt="Test Coverage" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/test_coverage">
24
+ </a>
25
+ </p>
26
+
27
+ This gem implements the observer pattern [[1]](https://en.wikipedia.org/wiki/Observer_pattern)[[2]](https://refactoring.guru/design-patterns/observer) (also known as publish/subscribe). It provides a simple mechanism for one object to inform a set of interested third-party objects when its state changes.
28
+
29
+ Ruby's standard library [has an abstraction](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html) that enables you to use this pattern. But its design can conflict with other mainstream libraries, like the [`ActiveModel`/`ActiveRecord`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changed), which also has the [`changed`](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html#method-i-changed) method. In this case, the behavior of the Stdlib will be compromised.
30
+
31
+ Because of this issue, I decided to create a gem that encapsulates the pattern without changing the object's implementation so much. The `Micro::Observers` includes just one instance method in the target class (its instance will be the observed subject/object).
32
+
33
+ > **Note:** Você entende português? 🇧🇷&nbsp;🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/serradura/u-observers/blob/main/README.pt-BR.md).
34
+
35
+ # Table of contents <!-- omit in toc -->
36
+ - [Installation](#installation)
37
+ - [Compatibility](#compatibility)
38
+ - [Usage](#usage)
39
+ - [Sharing a context with your observers](#sharing-a-context-with-your-observers)
40
+ - [Sharing data when notifying the observers](#sharing-data-when-notifying-the-observers)
41
+ - [What is a `Micro::Observers::Event`?](#what-is-a-microobserversevent)
42
+ - [Using a callable as an observer](#using-a-callable-as-an-observer)
43
+ - [Calling the observers](#calling-the-observers)
44
+ - [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
45
+ - [Defining observers that execute only once](#defining-observers-that-execute-only-once)
46
+ - [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
47
+ - [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
48
+ - [Detaching observers](#detaching-observers)
49
+ - [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
50
+ - [notify_observers_on()](#notify_observers_on)
51
+ - [notify_observers()](#notify_observers)
52
+ - [Development](#development)
53
+ - [Contributing](#contributing)
54
+ - [License](#license)
55
+ - [Code of Conduct](#code-of-conduct)
56
+
57
+ # Installation
58
+
59
+ Add this line to your application's Gemfile and `bundle install`:
10
60
 
11
61
  ```ruby
12
- gem 'micro-observers'
62
+ gem 'u-observers'
13
63
  ```
14
64
 
15
- And then execute:
65
+ # Compatibility
16
66
 
17
- $ bundle install
67
+ | u-observers | branch | ruby | activerecord |
68
+ | ----------- | ------- | -------- | ------------- |
69
+ | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
70
+ | 2.2.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
71
+ | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
18
72
 
19
- Or install it yourself as:
73
+ > **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
74
 
21
- $ gem install u-observers
75
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
22
76
 
23
77
  ## Usage
24
78
 
25
- TODO: Write usage instructions here
79
+ Any class with `Micro::Observers` module included can notify events to attached observers.
80
+
81
+ ```ruby
82
+ require 'securerandom'
83
+
84
+ class Order
85
+ include Micro::Observers
86
+
87
+ attr_reader :code
88
+
89
+ def initialize
90
+ @code, @status = SecureRandom.alphanumeric, :draft
91
+ end
92
+
93
+ def canceled?
94
+ @status == :canceled
95
+ end
96
+
97
+ def cancel!
98
+ return self if canceled?
99
+
100
+ @status = :canceled
101
+
102
+ observers.subject_changed!
103
+ observers.notify(:canceled) and return self
104
+ end
105
+ end
106
+
107
+ module OrderEvents
108
+ def self.canceled(order)
109
+ puts "The order #(#{order.code}) has been canceled."
110
+ end
111
+ end
112
+
113
+ order = Order.new
114
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
115
+
116
+ order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
117
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
118
+
119
+ order.canceled?
120
+ # false
121
+
122
+ order.cancel!
123
+ # The message below will be printed by the observer (OrderEvents):
124
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
125
+
126
+ order.canceled?
127
+ # true
128
+
129
+ order.observers.detach(OrderEvents) # detaching multiple observers. e.g. observers.detach(A, B, C)
130
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>
131
+
132
+ order.canceled?
133
+ # true
134
+
135
+ order.observers.subject_changed!
136
+ order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.
137
+ ```
138
+
139
+ **Highlights of the previous example:**
140
+
141
+ To avoid an undesired behavior, you need to mark the subject as changed before notifying your observers about some event.
142
+
143
+ You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
144
+
145
+ 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)`
146
+
147
+ 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.
148
+
149
+ ```ruby
150
+ order.observers.notify
151
+ # ArgumentError (no events (expected at least 1))
152
+ ```
153
+
154
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
155
+
156
+ ### Sharing a context with your observers
157
+
158
+ To share a context value (any kind of Ruby object) with one or more observers, you will need to use the `:context` keyword as the last argument of the `#attach` method. This feature gives you a unique opportunity to share a value in the attaching moment.
159
+
160
+ When the observer method receives two arguments, the first one will be the subject, and the second one an instance of `Micro::Observers::Event` that will have the given context value.
161
+
162
+ ```ruby
163
+ class Order
164
+ include Micro::Observers
165
+
166
+ def cancel!
167
+ observers.subject_changed!
168
+ observers.notify(:canceled)
169
+ self
170
+ end
171
+ end
172
+
173
+ module OrderEvents
174
+ def self.canceled(order, event)
175
+ puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
176
+ end
177
+ end
178
+
179
+ order = Order.new
180
+ order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
181
+ order.cancel!
182
+ # The message below will be printed by the observer (OrderEvents):
183
+ # The order #(70196221441820) has been canceled. (from: example #2)
184
+ ```
185
+
186
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
187
+
188
+ ### Sharing data when notifying the observers
189
+
190
+ As previously mentioned, the [`event context`](#sharing-a-context-with-your-observers) is a value that is stored when you attach your observer. But sometimes, it will be useful to send some additional data when broadcasting an event to the observers. The `event data` gives you this unique opportunity to share some value at the the notification moment.
191
+
192
+ ```ruby
193
+ class Order
194
+ include Micro::Observers
195
+ end
196
+
197
+ module OrderHandler
198
+ def self.changed(order, event)
199
+ puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
200
+ end
201
+ end
202
+
203
+ order = Order.new
204
+ order.observers.attach(OrderHandler, context: { from: 'example #3' })
205
+ order.observers.subject_changed!
206
+ order.observers.notify(:changed, data: 1)
207
+ # The message below will be printed by the observer (OrderHandler):
208
+ # The order #(70196221441820) received the number 1 from example #3.
209
+ ```
210
+
211
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
212
+
213
+ ### What is a `Micro::Observers::Event`?
214
+
215
+ The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
216
+
217
+ - `#name` will be the broadcasted event.
218
+ - `#subject` will be the observed subject.
219
+ - `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined in the moment that you attach the observer.
220
+ - `#data` will be [the value that was shared in the observers' notification](#sharing-data-when-notifying-the-observers).
221
+ - `#ctx` is an alias for the `#context` method.
222
+ - `#subj` is an alias for the `#subject` method.
223
+
224
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
225
+
226
+ ### Using a callable as an observer
227
+
228
+ The `observers.on()` method enables you to attach a callable as an observer.
229
+
230
+ Usually, a callable has a well-defined responsibility (do only one thing), because of this, it tends to be more [SRP (Single-responsibility principle)](https://en.wikipedia.org/wiki/Single-responsibility_principle). friendly than a conventional observer (that could have N methods to respond to different kinds of notification).
231
+
232
+ This method receives the below options:
233
+ 1. `:event` the expected event name.
234
+ 2. `:call` the callable object itself.
235
+ 3. `:with` (optional) it can define the value which will be used as the callable object's argument. So, if it is a `Proc`, a `Micro::Observers::Event` instance will be received as the `Proc` argument, and its output will be the callable argument. But if this option wasn't defined, the `Micro::Observers::Event` instance will be the callable argument.
236
+ 4. `:context` will be the context data that was defined in the moment that you attach the observer.
237
+
238
+ ```ruby
239
+ class Person
240
+ include Micro::Observers
241
+
242
+ attr_reader :name
243
+
244
+ def initialize(name)
245
+ @name = name
246
+ end
247
+
248
+ def name=(new_name)
249
+ return unless observers.subject_changed(new_name != @name)
250
+
251
+ @name = new_name
252
+
253
+ observers.notify(:name_has_been_changed)
254
+ end
255
+ end
256
+
257
+ PrintPersonName = -> (data) do
258
+ puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
259
+ end
260
+
261
+ person = Person.new('Rodrigo')
262
+
263
+ person.observers.on(
264
+ event: :name_has_been_changed,
265
+ call: PrintPersonName,
266
+ with: -> event { {person: event.subject, number: event.context} },
267
+ context: rand
268
+ )
269
+
270
+ person.name = 'Serradura'
271
+ # The message below will be printed by the observer (PrintPersonName):
272
+ # Person name: Serradura, number: 0.5018509191706862
273
+ ```
274
+
275
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
276
+
277
+ ### Calling the observers
278
+
279
+ You can use a callable (a class, module, or object that responds to the call method) to be your observers.
280
+ To do this, you only need to make use of the method `#call` instead of `#notify`.
281
+
282
+ ```ruby
283
+ class Order
284
+ include Micro::Observers
285
+
286
+ def cancel!
287
+ observers.subject_changed!
288
+ observers.call # in practice, this is a shortcut to observers.notify(:call)
289
+ self
290
+ end
291
+ end
292
+
293
+ NotifyAfterCancel = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
294
+
295
+ order = Order.new
296
+ order.observers.attach(NotifyAfterCancel)
297
+ order.cancel!
298
+ # The message below will be printed by the observer (NotifyAfterCancel):
299
+ # The order #(70196221441820) has been canceled.
300
+ ```
301
+
302
+ > **Note**: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.
303
+
304
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
305
+
306
+ ### Notifying observers without marking them as changed
307
+
308
+ This feature needs to be used with caution!
309
+
310
+ If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
311
+
312
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
313
+
314
+ ### Defining observers that execute only once
315
+
316
+ There are two ways to attach an observer and define it to be performed only once.
317
+
318
+ The first way to do this is passing the `perform_once: true` option to the `observers.attach()` method. e.g.
319
+
320
+ #### `observers.attach(*args, perform_once: true)`
321
+
322
+ ```ruby
323
+ class Order
324
+ include Micro::Observers
325
+
326
+ def cancel!
327
+ observers.notify!(:canceled)
328
+ end
329
+ end
330
+
331
+ module OrderNotifications
332
+ def self.canceled(order)
333
+ puts "The order #(#{order.object_id}) has been canceled."
334
+ end
335
+ end
336
+
337
+ order = Order.new
338
+ order.observers.attach(OrderNotifications, perform_once: true) # you can also pass an array of observers with this option
339
+
340
+ order.observers.some? # true
341
+ order.cancel! # The order #(70291642071660) has been canceled.
342
+
343
+ order.observers.some? # false
344
+ order.cancel! # Nothing will happen because there aren't observers.
345
+ ```
346
+
347
+ #### `observers.once(event:, call:, ...)`
348
+
349
+ The second way to achieve this is using `observers.once()` that has the same API of [`observers.on()`](#using-a-callable-as-an-observer). But the difference of the `#once()` method is that it will remove the observer after its execution.
350
+
351
+ ```ruby
352
+ class Order
353
+ include Micro::Observers
354
+
355
+ def cancel!
356
+ observers.notify!(:canceled)
357
+ end
358
+ end
359
+
360
+ module NotifyAfterCancel
361
+ def self.call(event)
362
+ puts "The order #(#{event.subject.object_id}) has been canceled."
363
+ end
364
+ end
365
+
366
+ order = Order.new
367
+ order.observers.once(event: :canceled, call: NotifyAfterCancel)
368
+
369
+ order.observers.some? # true
370
+ order.cancel! # The order #(70301497466060) has been canceled.
371
+
372
+ order.observers.some? # false
373
+ order.cancel! # Nothing will happen because there aren't observers.
374
+ ```
375
+
376
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
377
+
378
+ ### Detaching observers
379
+
380
+ As shown in the first example, you can use the `observers.detach()` to remove observers.
381
+
382
+ But, there is an alternative method to remove observer objects or remove callables by their event names. The method to do this is: `observers.off()`.
383
+
384
+ ```ruby
385
+ class Order
386
+ include Micro::Observers
387
+ end
388
+
389
+ NotifyAfterCancel = -> {}
390
+
391
+ module OrderNotifications
392
+ def self.canceled(_order)
393
+ end
394
+ end
395
+
396
+ order = Order.new
397
+ order.observers.on(event: :canceled, call: NotifyAfterCancel)
398
+ order.observers.attach(OrderNotifications)
399
+
400
+ order.observers.some? # true
401
+ order.observers.count # 2
402
+
403
+ order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
404
+ order.observers.some? # true
405
+ order.observers.count # 1
406
+
407
+ order.observers.off(OrderNotifications)
408
+ order.observers.some? # false
409
+ order.observers.count # 0
410
+ ```
411
+
412
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
413
+
414
+ ### ActiveRecord and ActiveModel integrations
415
+
416
+ To make use of this feature you need to require an additional module.
417
+
418
+ Gemfile example:
419
+ ```ruby
420
+ gem 'u-observers', require: 'u-observers/for/active_record'
421
+ ```
422
+
423
+ 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:
424
+
425
+ #### notify_observers_on()
426
+
427
+ The `notify_observers_on` allows you to define one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
428
+
429
+ ```ruby
430
+ class Post < ActiveRecord::Base
431
+ include ::Micro::Observers::For::ActiveRecord
432
+
433
+ notify_observers_on(:after_commit) # using multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
434
+
435
+ # The method above does the same as the commented example below.
436
+ #
437
+ # after_commit do |record|
438
+ # record.subject_changed!
439
+ # record.notify(:after_commit)
440
+ # end
441
+ end
442
+
443
+ module TitlePrinter
444
+ def self.after_commit(post)
445
+ puts "Title: #{post.title}"
446
+ end
447
+ end
448
+
449
+ module TitlePrinterWithContext
450
+ def self.after_commit(post, event)
451
+ puts "Title: #{post.title} (from: #{event.context[:from]})"
452
+ end
453
+ end
454
+
455
+ Post.transaction do
456
+ post = Post.new(title: 'Hello world')
457
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
458
+ post.save
459
+ end
460
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
461
+ # Title: Hello world
462
+ # Title: Hello world (from: example #6)
463
+ ```
464
+
465
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
466
+
467
+ #### notify_observers()
468
+
469
+ The `notify_observers` allows you to define one or more *events*, that will be used to notify after the execution of some `ActiveModel`/`ActiveRecord` callback.
470
+
471
+ ```ruby
472
+ class Post < ActiveRecord::Base
473
+ include ::Micro::Observers::For::ActiveRecord
474
+
475
+ after_commit(&notify_observers(:transaction_completed))
476
+
477
+ # The method above does the same as the commented example below.
478
+ #
479
+ # after_commit do |record|
480
+ # record.subject_changed!
481
+ # record.notify(:transaction_completed)
482
+ # end
483
+ end
484
+
485
+ module TitlePrinter
486
+ def self.transaction_completed(post)
487
+ puts("Title: #{post.title}")
488
+ end
489
+ end
490
+
491
+ module TitlePrinterWithContext
492
+ def self.transaction_completed(post, event)
493
+ puts("Title: #{post.title} (from: #{event.ctx[:from]})")
494
+ end
495
+ end
496
+
497
+ Post.transaction do
498
+ post = Post.new(title: 'Olá mundo')
499
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
500
+ post.save
501
+ end
502
+ # The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
503
+ # Title: Olá mundo
504
+ # Title: Olá mundo (from: example #5)
505
+ ```
506
+
507
+ > **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.
508
+
509
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
26
510
 
27
511
  ## Development
28
512