reactive_observers 0.0.1.pre
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 +7 -0
- data/.gitignore +11 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +208 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/reactive_observers/base.rb +61 -0
- data/lib/reactive_observers/configuration.rb +18 -0
- data/lib/reactive_observers/database_adapters/abstract_adapter.rb +34 -0
- data/lib/reactive_observers/database_adapters/factory.rb +38 -0
- data/lib/reactive_observers/database_adapters/postgresql_adapter.rb +28 -0
- data/lib/reactive_observers/observable/base.rb +102 -0
- data/lib/reactive_observers/observable/db_listener.rb +22 -0
- data/lib/reactive_observers/observable/filtering.rb +40 -0
- data/lib/reactive_observers/observable/notification.rb +75 -0
- data/lib/reactive_observers/observable/removing.rb +21 -0
- data/lib/reactive_observers/observer/container.rb +66 -0
- data/lib/reactive_observers/observer/container_comparator.rb +43 -0
- data/lib/reactive_observers/observer/container_validator.rb +41 -0
- data/lib/reactive_observers/version.rb +5 -0
- data/lib/reactive_observers.rb +23 -0
- data/reactive_observers.gemspec +35 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6be66bbad98da7ba75c596f536c7d20eb8abf6b376c3d53db9a2c8e2fe925128
|
4
|
+
data.tar.gz: 13a0090307196c9295bee4640fd57a938276063c5c4c7a508f3d7dee46694400
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 46689eaebf879db1bb5c256405383f13bc406295abce70932d1b1328914fe6c8b09907dc08b7ae57f6e70dd7990035b4b2dab3d6754b62eac8e3c3d035b071a3
|
7
|
+
data.tar.gz: 11bb559400d8313adf10754cc409e2c9453612e4b11c469e52c0af7f865c551aa948b1953b8f661c095332158a1c18a5f7d8e5625c6da4d6107416f0b20c89dc
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
reactive_observers
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.6.3
|
data/.travis.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
---
|
2
|
+
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
sudo: required
|
5
|
+
rvm:
|
6
|
+
- 2.6.3
|
7
|
+
services:
|
8
|
+
- postgresql
|
9
|
+
addons:
|
10
|
+
postgresql: 9.6
|
11
|
+
before_script:
|
12
|
+
- sudo apt-get -qq update
|
13
|
+
- sudo apt-get install -y postgresql-9.6-postgis-2.4
|
14
|
+
- psql -U postgres -c 'create database test'
|
15
|
+
- psql -U postgres -d test -c 'create extension postgis'
|
16
|
+
before_install: gem install bundler -v 2.1.0.pre.3
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in reactive_observers.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "rake", "~> 12.0"
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem "minitest", "~> 5.0"
|
10
|
+
gem 'minitest-reporters'
|
11
|
+
gem 'minitest-stub_any_instance'
|
12
|
+
gem 'simplecov'
|
13
|
+
gem 'codecov'
|
14
|
+
|
15
|
+
gem 'pg'
|
16
|
+
gem 'activerecord-postgis-adapter'
|
17
|
+
gem 'sqlite3'
|
18
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 martintomas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# ReactiveObservers
|
2
|
+
|
3
|
+
[](https://travis-ci.com/martintomas/reactive_observers)
|
4
|
+
[](https://codecov.io/gh/martintomas/reactive_observers)
|
5
|
+
|
6
|
+
This gem can make observer from every possible class or object. Observer relation can be defined at Class level and processed dynamically when appropriate record changes. Observable module is using build in Active Record hooks or database triggers which can be turned on for specified tables in multiple App environment.
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
class Topic < ActiveRecord::Base; end
|
10
|
+
class CustomObserver
|
11
|
+
include ReactiveObservers::Base
|
12
|
+
|
13
|
+
def changed(topic, **observer); end
|
14
|
+
end
|
15
|
+
|
16
|
+
# possible usage of observer
|
17
|
+
# when observer is defined at class level, observer object is initialized when observed record changes
|
18
|
+
CustomObserver.observe(:topics) # observer klass is observing Topic (Active Record klass)
|
19
|
+
CustomObserver.observe(Topic.first) # observer is observing specific topic
|
20
|
+
CustomObserver.new.observe(:topics) # specific observer is observing Topic
|
21
|
+
CustomObserver.new.observe(Topic.first) # specific observer is observing specific topic
|
22
|
+
```
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'reactive_observers'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle install
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install reactive_observers
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Observing relation can be initialized between object/class pairs. Observer can be any class or object. Observable has to be always Active Record class or object. It is recommended to define class observers as often as possible, because It enables to observe all active records independently and initialize all required data after observer is triggered.
|
43
|
+
|
44
|
+
Every class can be transformed to observer just by including following module:
|
45
|
+
|
46
|
+
$ include ReactiveObservers::Base
|
47
|
+
|
48
|
+
Class has access to `observe` method now and can observe any active record object or class. Registering of observer can be done for example by:
|
49
|
+
|
50
|
+
$ YourClass.observe(ActiveRecord::Base)
|
51
|
+
|
52
|
+
Observe method accepts several different arguments which are:
|
53
|
+
* filtering options
|
54
|
+
* `on` - observer is notified only when specific types of action happens, for example: `on: [:create, :update]`
|
55
|
+
* `fields` - observer is notified only when specified active record attributes are changed, for example: `fields: [:first_name, :last_name]`
|
56
|
+
* `only` - accepts Proc and can do additional complex filtering, for example: `only: ->(active_record) { active_record.type == 'ObservableType' }`
|
57
|
+
* active options
|
58
|
+
* `trigger` - can be symbol or Proc and defines action which is called on observer, for example: `trigger: :recompute_statistics`. Default value, which can be changed through configuration, is `:changed`.
|
59
|
+
* `notify` - can be Symbol or Proc and defines action which is used to initialize observes class, for example: `notify: :load_all_dependent_objects`
|
60
|
+
* `refine` - accepts Proc and defines operation which is done with active record object before observer is called, for example: `refine: ->(active_record) { active_record.topics }`
|
61
|
+
* additional options
|
62
|
+
* `context` - observer can be registered with context information which is provided back from observed object when notification happens. Example of such option can be for example: `context: :topic`
|
63
|
+
|
64
|
+
### Active Record as Observer
|
65
|
+
|
66
|
+
Every Active Record class can be transformed to observer - It can observe and be observed at same time. Let's have following example:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class Comment < ActiveRecord::Base; end
|
70
|
+
class Topic < ActiveRecord::Base
|
71
|
+
include ReactiveObservers::Base
|
72
|
+
|
73
|
+
# register observer for comments
|
74
|
+
# when Comment is created, call update_topic_dashboard of Topic
|
75
|
+
# notify param tells observed objects which topic should be called
|
76
|
+
observe :comments, on: :create, trigger: :update_topic_dashboard, notify: ->(comment) { comment.topic }
|
77
|
+
|
78
|
+
def update_topic_dashboard(**observer); end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Observer as part of any class
|
83
|
+
|
84
|
+
Every possible class can be transformed to observer. Let's define simple class as our first example:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class TopicsStatisticService
|
88
|
+
include ReactiveObservers::Base
|
89
|
+
|
90
|
+
observe :topics, fields: :active_users, trigger: :recompute_statistics
|
91
|
+
observe :comments, on: :create, trigger: :recompute_statistics, refine: ->(comment) { comment.topic }
|
92
|
+
|
93
|
+
# you can recompute statistics for topic which has changed
|
94
|
+
def recompute_statistics(topic, **observer); end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
Class observers can become a bit tricky when initialization method is defined inside observer.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class TopicStatisticService
|
102
|
+
include ReactiveObservers::Base
|
103
|
+
|
104
|
+
observe :topics, fields: :active_users, trigger: :recompute_statistics, only: -> (topic) { topic.active? },
|
105
|
+
notify: ->(topic) { TopicStatisticService.new(topic, topic.active_comments) }
|
106
|
+
|
107
|
+
# observed object will use notify param to instanciate TopicStatisticService object
|
108
|
+
# this param is required because initialize requires additional arguments
|
109
|
+
def initialize(topic, active_comments); end
|
110
|
+
|
111
|
+
def recompute_statistics(topic, **observer); end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Initialize method of class can quickly get out of the hand and It can be impossible to initialize it only with observed object information. Fortunately, observing can be defined at object level which doesn't require initialization.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class ComplexStatisticsService
|
119
|
+
include ReactiveObservers::Base
|
120
|
+
|
121
|
+
# both params are unknown and It is impossible to initialize ComplexStatisticsService with observed data
|
122
|
+
def initialize(unknown_param1, unknown_param2); end
|
123
|
+
|
124
|
+
def changed(record, **observer); end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Class.observe pattern cannot be used in this case, because observed record cannot probably instantiate this service.
|
128
|
+
# You can register specific service as observer
|
129
|
+
ComplexStatisticsService.new(param1, param2).observe(:topics) # all topics will be observed
|
130
|
+
ComplexStatisticsService.new(param1, param2).observe(Topic.first) # only first topic will be observed
|
131
|
+
```
|
132
|
+
|
133
|
+
### Observer Class
|
134
|
+
|
135
|
+
Implementation of specific observers can be encapsulated into one class which makes future maintenance quite simple - in reality, this is preferred way how to define observers. Quite dummy example of such observer class can be:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
# Example of Activity Observer
|
139
|
+
# observe appropriate record and recompute topic activity when data changes
|
140
|
+
class ActivityObserver
|
141
|
+
include ReactiveObservers::Base
|
142
|
+
|
143
|
+
observe :topics, fields: :last_activity
|
144
|
+
observe :comments, refine: ->(comment) { comment.topic }
|
145
|
+
observe :users, fields: :open_topic, refine: ->(user) { user.open_topic }
|
146
|
+
observe :images, on: :create, trigger: :image_uploaded
|
147
|
+
|
148
|
+
# activity data at topic changed, recompute it
|
149
|
+
def changed(topic, **observer); end
|
150
|
+
|
151
|
+
# image upload requires specifies approach, use special trigger for it
|
152
|
+
def image_uploaded(image, **observer); end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Remove Observer
|
157
|
+
|
158
|
+
It is possible to remove observers from observed class or object at any time.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
Topic.remove_observer(ActivityObserver) # remove activity observer from Topic class
|
162
|
+
Topic.first.remove_observer(ActivityObserver) # remove observer from first topic
|
163
|
+
Topic.remove_observer(observing_service) # observer can be also object and this observer can be removed same way as class observer
|
164
|
+
|
165
|
+
# remove_observer method accepts additional arguments that specifies which observers should be removed
|
166
|
+
Topic.remove_observer(ActivityObserver, trigger: :image_uploaded) # only observer with appropriate trigger will be removed
|
167
|
+
Topic.remove_observer(ActivityObserver, trigger: :image_uploaded, on: [:create, :update])
|
168
|
+
Topic.remove_observer(ActivityObserver, fields: [:first_name, :last_name])
|
169
|
+
Topic.remove_observer(ActivityObserver, notify: :prepare_observer)
|
170
|
+
Topic.remove_observer(ActivityObserver, context: :topic) # only observer with appropriate context will be removed
|
171
|
+
```
|
172
|
+
|
173
|
+
### Database Triggers (Advanced)
|
174
|
+
|
175
|
+
Observed data can be sometimes manipulated by several different sources - for example different apps can update it. Unfortunately, active record hooks in our Rails App cannot catch such change - which can cause that observers are not notified. For this purpose, this gem supports observers which use database triggers.
|
176
|
+
|
177
|
+
Only __PostgreSQL database__ is supported now but You are welcomed to add other database adapters!
|
178
|
+
|
179
|
+
To enable database observers, following configuration needs to be put into initializers:
|
180
|
+
|
181
|
+
ReactiveObservers.configure do |config|
|
182
|
+
config.observed_tables = [:topics] # names of tables which should be observed
|
183
|
+
end
|
184
|
+
|
185
|
+
It is also required to create appropriate database triggers. __TODO:__ prepare jobs that generates appropriate triggers for defined tables.
|
186
|
+
|
187
|
+
Gem listens on `TG_TABLE_NAME_notices` which for example means that observer for topic table will listen on `topic_notices`. This default behaviour can be changed at configuration:
|
188
|
+
|
189
|
+
$ ReactiveObservers.configure { |config| config.listening_job_name = "%{table_name}_notices" }
|
190
|
+
|
191
|
+
Data obtained through trigger are forwarded to observers, but can be also used for different purposes (not just observing). It is possible to register any method that can process trigger data at any active record model:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class Topic < ActiveRecord::Base
|
195
|
+
register_observer_listener :process_trigger
|
196
|
+
|
197
|
+
def self.process_trigger(data); end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
## Contributing
|
202
|
+
|
203
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/martintomas/reactive_observers. This project is intended to be a safe and welcoming space for collaboration.
|
204
|
+
|
205
|
+
## License
|
206
|
+
|
207
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
208
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "reactive_observers"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'reactive_observers/observer/container'
|
5
|
+
|
6
|
+
module ReactiveObservers
|
7
|
+
# add observe methods to appropriate class
|
8
|
+
#
|
9
|
+
# class CustomObserver
|
10
|
+
# include ReactiveObservers::Base
|
11
|
+
#
|
12
|
+
# def changed(topic, **observer); end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
module Base
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
# create class observer for provided active record object or class
|
20
|
+
#
|
21
|
+
# CustomObserver.observe(:topics) # observer is observing Topic klass
|
22
|
+
# CustomObserver.observe(Topic.first) # observer is observing specific topic
|
23
|
+
#
|
24
|
+
# @param observed [Symbol, Object] observed object or symbol defining specific ActiveRecord class
|
25
|
+
# @param refine [Proc] lambda or Proc function defining observed object pre-process before It is sent to observer
|
26
|
+
# @param trigger [Proc, Symbol] function that is triggered inside observer during notification
|
27
|
+
# @param notify [Proc, Symbol] function that is used to initialize appropriate observer objects
|
28
|
+
# @param options [Hash] additional arguments such as: on, only, fields or context
|
29
|
+
# @return [ReactiveObservers::Observer::Container] observer
|
30
|
+
def observe(observed, refine: nil, trigger: ReactiveObservers.configuration.default_trigger, notify: nil, **options)
|
31
|
+
add_observer_to_observable self, observed, options.merge(refine: refine, trigger: trigger, notify: notify)
|
32
|
+
end
|
33
|
+
|
34
|
+
# register observer at observed entity
|
35
|
+
# @param observer [Class, Object]
|
36
|
+
# @param observed [Symbol, Object]
|
37
|
+
# @param options [Hash]
|
38
|
+
# @return [ReactiveObservers::Observer::Container] observer
|
39
|
+
def add_observer_to_observable(observer, observed, options)
|
40
|
+
ReactiveObservers::Observer::Container.new(observer, observed, options).tap do |observer_container|
|
41
|
+
observer_container.observed_klass.register_observer observer_container
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# create object observer for provided active record object or class
|
47
|
+
#
|
48
|
+
# CustomObserver.new.observe(:topics) # observer is observing Topic klass
|
49
|
+
# CustomObserver.new.observe(Topic.first) # observer is observing specific topic
|
50
|
+
#
|
51
|
+
# @param observed [Symbol, Object] observed object or symbol defining specific ActiveRecord class
|
52
|
+
# @param refine [Proc] lambda or Proc function defining observed object pre-process before It is sent to observer
|
53
|
+
# @param trigger [Proc, Symbol] function that is triggered inside observer during notification
|
54
|
+
# @param notify [Proc, Symbol] function that is used to initialize appropriate observer objects
|
55
|
+
# @param options [Hash] additional arguments such as: on, only, fields or context
|
56
|
+
# @return [ReactiveObservers::Observer::Container] observer
|
57
|
+
def observe(observed, refine: nil, trigger: ReactiveObservers.configuration.default_trigger, notify: nil, **options)
|
58
|
+
self.class.add_observer_to_observable self, observed, options.merge(refine: refine, trigger: trigger, notify: notify)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
attr_accessor :listening_job_name, :observed_tables, :default_trigger
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
reset!
|
10
|
+
end
|
11
|
+
|
12
|
+
def reset!
|
13
|
+
@listening_job_name = "%{table_name}_notices" # trigger listens for these type of notices
|
14
|
+
@observed_tables = [] # these tables are observed at database level
|
15
|
+
@default_trigger = :changed # default name of method that is called inside observer during notification
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
module DatabaseAdapters
|
5
|
+
class AbstractAdapter
|
6
|
+
def initialize(configuration, klasses)
|
7
|
+
@configuration = configuration
|
8
|
+
@klasses = klasses
|
9
|
+
end
|
10
|
+
|
11
|
+
def start_listening
|
12
|
+
@klasses.each { |klass| create_listening_job_for klass }
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop_listening
|
16
|
+
@klasses.each { |klass| stop_listening_job_for klass }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_listening_job_for(klass)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop_listening_job_for(klass)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_notification_for(data, klass)
|
30
|
+
klass.observer_listener_services.each { |service| klass.method(service).call data }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'reactive_observers/database_adapters/postgresql_adapter'
|
4
|
+
|
5
|
+
module ReactiveObservers
|
6
|
+
module DatabaseAdapters
|
7
|
+
class Factory
|
8
|
+
def initialize(configuration)
|
9
|
+
@configuration = configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize_observer_listeners
|
13
|
+
collect_database_adapters.each do |database_adapter, klasses|
|
14
|
+
case database_adapter
|
15
|
+
when 'PostgreSQL'
|
16
|
+
PostgreSQLAdapter.new(@configuration, klasses).start_listening
|
17
|
+
when 'PostGIS'
|
18
|
+
PostgreSQLAdapter.new(@configuration, klasses).start_listening
|
19
|
+
else
|
20
|
+
raise StandardError, "Reactive observers cannot be run with this database adapter: #{database_adapter}!"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def collect_database_adapters
|
28
|
+
{}.tap do |result|
|
29
|
+
@configuration.observed_tables.map do |observed_table|
|
30
|
+
klass = observed_table.to_s.classify.constantize
|
31
|
+
adapter = klass.connection.adapter_name
|
32
|
+
result[adapter] = (result[adapter] || []) << klass
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'reactive_observers/database_adapters/abstract_adapter'
|
4
|
+
|
5
|
+
module ReactiveObservers
|
6
|
+
module DatabaseAdapters
|
7
|
+
class PostgreSQLAdapter < AbstractAdapter
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def create_listening_job_for(klass)
|
12
|
+
Thread.new do
|
13
|
+
klass.connection.execute "LISTEN #{ @configuration.listening_job_name % { table_name: klass.table_name }}"
|
14
|
+
loop do
|
15
|
+
klass.connection.raw_connection.wait_for_notify do |event, pid, payload|
|
16
|
+
data = JSON.parse payload, symbolize_names: true
|
17
|
+
process_notification_for data, klass
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end.abort_on_exception = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop_listening_job_for(klass)
|
24
|
+
klass.connection.execute "UNLISTEN #{ @configuration.listening_job_name % { table_name: klass.table_name }}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'reactive_observers/observable/db_listener'
|
4
|
+
require 'reactive_observers/observable/notification'
|
5
|
+
require 'reactive_observers/observable/removing'
|
6
|
+
|
7
|
+
require 'active_support/concern'
|
8
|
+
|
9
|
+
module ReactiveObservers
|
10
|
+
module Observable
|
11
|
+
# enables class to be observable
|
12
|
+
# automatically included to ActiveRecord::Base
|
13
|
+
module Base
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
include Observable::DbListener
|
16
|
+
|
17
|
+
included do
|
18
|
+
class_attribute :active_observers
|
19
|
+
self.active_observers = []
|
20
|
+
register_observer_listener :process_observer_notification
|
21
|
+
|
22
|
+
after_create do
|
23
|
+
process_observer_hook_notification :create
|
24
|
+
end
|
25
|
+
|
26
|
+
after_update do
|
27
|
+
process_observer_hook_notification :update, diff: previous_changes.each_with_object({}) { |(k, v), r| r[k] = v.first }
|
28
|
+
end
|
29
|
+
|
30
|
+
after_destroy do
|
31
|
+
process_observer_hook_notification :destroy
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class_methods do
|
36
|
+
# register observer to this class
|
37
|
+
# @param observer [ReactiveObservers::Observer::Container]
|
38
|
+
# @return [Array] active observers
|
39
|
+
def register_observer(observer)
|
40
|
+
return if active_observers.any? { |active_observer| active_observer.compare.full? observer }
|
41
|
+
|
42
|
+
active_observers << observer
|
43
|
+
end
|
44
|
+
|
45
|
+
# remove observer for specific object
|
46
|
+
#
|
47
|
+
# Topic.remove_observer(ActivityObserver) # remove observer from Topic
|
48
|
+
# Topic.remove_observer(observing_service) # removed observer can be also object
|
49
|
+
#
|
50
|
+
# @param observer [Class, Object] observer that should be removed
|
51
|
+
# @param options [Hash] additional options that specifies which observers should be removed
|
52
|
+
# @return [Array] still active observers
|
53
|
+
def remove_observer(observer, **options)
|
54
|
+
Observable::Removing.new(active_observers, observer, options).perform
|
55
|
+
end
|
56
|
+
|
57
|
+
# process notification from db trigger
|
58
|
+
# @param data [Hash] data obtain from db trigger
|
59
|
+
def process_observer_notification(data)
|
60
|
+
return if active_observers.blank?
|
61
|
+
|
62
|
+
if data[:action] == 'INSERT'
|
63
|
+
find(data[:id]).process_observer_notifications :create
|
64
|
+
elsif data[:action] == 'UPDATE'
|
65
|
+
find(data[:id]).process_observer_notifications :update, diff: data[:diff]
|
66
|
+
elsif data[:action] == 'DELETE'
|
67
|
+
new(data[:diff]).process_observer_notifications :destroy
|
68
|
+
else
|
69
|
+
raise StandardError, "Notification from db returned unknown action: #{data[:action]}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# remove observer for specific object
|
75
|
+
#
|
76
|
+
# Topic.first.remove_observer(ActivityObserver) # remove observer from first topic
|
77
|
+
#
|
78
|
+
# @param observer [Class, Object] observer that should be removed
|
79
|
+
# @param options [Hash] additional options that specifies which observers should be removed
|
80
|
+
# @return [Array] still active observers
|
81
|
+
def remove_observer(observer, **options)
|
82
|
+
self.class.remove_observer observer, options.merge(constrain: [id])
|
83
|
+
end
|
84
|
+
|
85
|
+
# process notification from ActiveRecord hooks
|
86
|
+
# @param action [Symbol]
|
87
|
+
# @param options [Hash]
|
88
|
+
def process_observer_hook_notification(action, **options)
|
89
|
+
return if ReactiveObservers.configuration.observed_tables.include?(self.class.table_name.to_sym) || self.class.active_observers.blank?
|
90
|
+
|
91
|
+
process_observer_notifications action, **options
|
92
|
+
end
|
93
|
+
|
94
|
+
# process observer notification
|
95
|
+
# @param action [Symbol]
|
96
|
+
# @param options [Hash]
|
97
|
+
def process_observer_notifications(action, **options)
|
98
|
+
Observable::Notification.new(self, self.class.active_observers, action, options).perform
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module ReactiveObservers
|
6
|
+
module Observable
|
7
|
+
module DbListener
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :observer_listener_services
|
12
|
+
self.observer_listener_services = []
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def register_observer_listener(method_name)
|
17
|
+
observer_listener_services << method_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
module Observable
|
5
|
+
class Filtering
|
6
|
+
def initialize(observed_object_id, observers, action, options)
|
7
|
+
@observed_object_id = observed_object_id
|
8
|
+
@observers = observers
|
9
|
+
@action = action
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
@observers.select do |observer|
|
15
|
+
filter_action(observer) && filter_record_constrains(observer) && filter_fields(observer)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def filter_action(observer)
|
22
|
+
observer.on.blank? || observer.on.include?(@action)
|
23
|
+
end
|
24
|
+
|
25
|
+
def filter_record_constrains(observer)
|
26
|
+
observer.constrain.blank? || observer.constrain.include?(@observed_object_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter_fields(observer)
|
30
|
+
return true unless @action == :update && @options[:diff].present?
|
31
|
+
|
32
|
+
observer.fields.blank? || (observer.fields & changed_fields).length.positive?
|
33
|
+
end
|
34
|
+
|
35
|
+
def changed_fields
|
36
|
+
@changed_fields ||= @options[:diff].keys.map &:to_sym
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'reactive_observers/observable/filtering'
|
4
|
+
|
5
|
+
module ReactiveObservers
|
6
|
+
module Observable
|
7
|
+
class Notification
|
8
|
+
def initialize(observed_object, observers, action, options)
|
9
|
+
@observed_object = observed_object
|
10
|
+
@observers = observers
|
11
|
+
@action = action
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
filter_observers.each do |observer|
|
17
|
+
next if observer.only.present? && !observer.only.call(@observed_object)
|
18
|
+
|
19
|
+
trigger_actions_for observer, Array.wrap(refine_observer_records_for(observer))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def filter_observers
|
26
|
+
@filtered_observers ||= Filtering.new(@observed_object.id, @observers, @action, @options).perform
|
27
|
+
end
|
28
|
+
|
29
|
+
def trigger_actions_for(observer, records)
|
30
|
+
records.each do |record|
|
31
|
+
Array.wrap(observer_objects_for(observer, record)).each do |observer_object|
|
32
|
+
observer_object = observer_simplification?(observer, observer_object, record) ? record : observer_object
|
33
|
+
trigger_observer_action_for observer, observer_object, record
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def trigger_observer_action_for(observer, observer_object, record)
|
39
|
+
trigger = build_proc_for observer.trigger, observer_object
|
40
|
+
return trigger.call observer.to_h if observer_object == record
|
41
|
+
|
42
|
+
trigger.call record, observer.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
def refine_observer_records_for(observer)
|
46
|
+
return @observed_object if observer.refine.blank?
|
47
|
+
|
48
|
+
observer.refine.call @observed_object
|
49
|
+
end
|
50
|
+
|
51
|
+
def observer_objects_for(observer, record)
|
52
|
+
return observer_objects_from_klass observer, record if observer.klass_observer?
|
53
|
+
return build_proc_for(observer.notify, observer.observer).call(observer.observer, record) if observer.notify.present?
|
54
|
+
|
55
|
+
observer.observer
|
56
|
+
end
|
57
|
+
|
58
|
+
def observer_objects_from_klass(observer, record)
|
59
|
+
return build_proc_for(observer.notify, observer.observer).call(record) if observer.notify.present?
|
60
|
+
|
61
|
+
observer.observer.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_proc_for(variable, object)
|
65
|
+
return variable unless variable.is_a? Symbol
|
66
|
+
|
67
|
+
object.method variable
|
68
|
+
end
|
69
|
+
|
70
|
+
def observer_simplification?(observer, record, observer_object)
|
71
|
+
(observer_object == record) || (observer.klass_observer? && observer_object.is_a?(record.class))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
module Observable
|
5
|
+
class Removing
|
6
|
+
REQUIRED_FIELDS = %i[klass object].freeze
|
7
|
+
|
8
|
+
def initialize(active_observers, observer, removing_options)
|
9
|
+
@active_observers = active_observers
|
10
|
+
@observer = observer
|
11
|
+
@removing_options = removing_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
@active_observers.delete_if do |active_observer|
|
16
|
+
active_observer.compare.partial? @observer, @removing_options
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'reactive_observers/observer/container_validator'
|
4
|
+
require 'reactive_observers/observer/container_comparator'
|
5
|
+
|
6
|
+
module ReactiveObservers
|
7
|
+
module Observer
|
8
|
+
class Container
|
9
|
+
|
10
|
+
attr_accessor :observer, :observed
|
11
|
+
attr_accessor :trigger, :notify, :refine, :context
|
12
|
+
attr_accessor :fields, :on, :only, :constrain
|
13
|
+
|
14
|
+
def initialize(observer, observed, options)
|
15
|
+
@observer = observer
|
16
|
+
@observed = observed.is_a?(Symbol) ? observed.to_s.classify.constantize : observed
|
17
|
+
@on = Array.wrap options[:on]
|
18
|
+
@fields = Array.wrap options[:fields]
|
19
|
+
@only = options[:only]
|
20
|
+
@trigger = options[:trigger] || ReactiveObservers.configuration.default_trigger
|
21
|
+
@notify = options[:notify]
|
22
|
+
@refine = options[:refine]
|
23
|
+
@context = options[:context]
|
24
|
+
ReactiveObservers::Observer::ContainerValidator.new(self).run_validations!
|
25
|
+
@constrain = load_observer_constrains
|
26
|
+
end
|
27
|
+
|
28
|
+
def compare
|
29
|
+
ReactiveObservers::Observer::ContainerComparator.new(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def klass_observer?
|
33
|
+
@observer.is_a? Class
|
34
|
+
end
|
35
|
+
|
36
|
+
def klass_observed?
|
37
|
+
@observed.is_a? Class
|
38
|
+
end
|
39
|
+
|
40
|
+
def observer_klass
|
41
|
+
return @observer if klass_observer?
|
42
|
+
|
43
|
+
@observer.class
|
44
|
+
end
|
45
|
+
|
46
|
+
def observed_klass
|
47
|
+
return @observed if klass_observed?
|
48
|
+
|
49
|
+
@observed.class
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h
|
53
|
+
{ observer: @observer, observed: @observed, fields: @fields, on: @on, only: @only, constrain: @constrain,
|
54
|
+
trigger: @trigger, refine: @refine, notify: @notify, context: @context}
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def load_observer_constrains
|
60
|
+
return [] if @observed.is_a?(Class)
|
61
|
+
|
62
|
+
[@observed.id]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
module Observer
|
5
|
+
class ContainerComparator
|
6
|
+
def initialize(observer)
|
7
|
+
@observer = observer
|
8
|
+
end
|
9
|
+
|
10
|
+
def partial?(observer, options)
|
11
|
+
@observer.observer == observer &&
|
12
|
+
array_compare_of(@observer.fields, options[:fields]) &&
|
13
|
+
array_compare_of(@observer.on, options[:on]) &&
|
14
|
+
context_compare_with(options[:context]) &&
|
15
|
+
constrain_compare_with(options[:constrain])
|
16
|
+
end
|
17
|
+
|
18
|
+
def full?(observer)
|
19
|
+
partial?(observer.observer, fields: observer.fields, on: observer.on, constrain: observer.constrain) &&
|
20
|
+
@observer.observed == observer.observed &&
|
21
|
+
@observer.trigger == observer.trigger &&
|
22
|
+
@observer.notify == observer.notify &&
|
23
|
+
@observer.refine == observer.refine &&
|
24
|
+
@observer.only == observer.only
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def array_compare_of(argument, option_value)
|
30
|
+
argument.blank? || option_value.blank? || (argument & Array.wrap(option_value)).length.positive?
|
31
|
+
end
|
32
|
+
|
33
|
+
def constrain_compare_with(value)
|
34
|
+
value = Array.wrap value
|
35
|
+
value.blank? || @observer.constrain == value || (@observer.constrain & value).length.positive?
|
36
|
+
end
|
37
|
+
|
38
|
+
def context_compare_with(value)
|
39
|
+
value.blank? || @observer.context == value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveObservers
|
4
|
+
module Observer
|
5
|
+
class ContainerValidator
|
6
|
+
def initialize(observer)
|
7
|
+
@observer = observer
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_validations!
|
11
|
+
validate_observe_trigger!
|
12
|
+
validate_observe_notification!
|
13
|
+
validate_observe_active_record!
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_observe_trigger!
|
20
|
+
return unless @observer.trigger.is_a?(Symbol) && !@observer.observer_klass.method_defined?(@observer.trigger)
|
21
|
+
|
22
|
+
raise ArgumentError, "Class #{@observer.observer_klass.name} is missing required observed method #{@observer.trigger}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_observe_notification!
|
26
|
+
return if @observer.notify.present? || !@observer.klass_observer?
|
27
|
+
|
28
|
+
@observer.observer.new
|
29
|
+
rescue ArgumentError
|
30
|
+
raise ArgumentError, "Notify parameter is required for observer class #{@observer.observer_klass.name} which has complex initialization"
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_observe_active_record!
|
34
|
+
return if (!@observer.klass_observed? && @observer.observed.is_a?(ActiveRecord::Base)) ||
|
35
|
+
(@observer.klass_observed? && @observer.observed <= ActiveRecord::Base)
|
36
|
+
|
37
|
+
raise ArgumentError, "Class #{@observer.observed_klass.name} is not Active Record class"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'reactive_observers/version'
|
2
|
+
require 'reactive_observers/configuration'
|
3
|
+
require 'reactive_observers/base'
|
4
|
+
require 'reactive_observers/observable/base'
|
5
|
+
require 'reactive_observers/database_adapters/factory'
|
6
|
+
|
7
|
+
require 'active_record'
|
8
|
+
|
9
|
+
module ReactiveObservers
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
mattr_accessor :configuration, default: Configuration.new
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
self.configuration ||= Configuration.new
|
16
|
+
yield(configuration) if block_given?
|
17
|
+
DatabaseAdapters::Factory.new(configuration).initialize_observer_listeners
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ActiveRecord::Base
|
22
|
+
include ReactiveObservers::Observable::Base
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'lib/reactive_observers/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "reactive_observers"
|
5
|
+
spec.version = ReactiveObservers::VERSION
|
6
|
+
spec.authors = ["martintomas"]
|
7
|
+
spec.email = ["tomas@jchsoft.cz"]
|
8
|
+
|
9
|
+
spec.summary = %q{Observe Active Record classes and records reactive way!}
|
10
|
+
spec.description = %q{Every class or object can be transformed to observer and dynamically react to data changes across several models. Observable module is using build in Active Record hooks or database triggers which can be turned on in multiple App environment. }
|
11
|
+
spec.homepage = "https://github.com/martintomas/reactive_observers.git"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
|
+
|
15
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/martintomas/reactive_observers.git"
|
19
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "activerecord", ">= 5.0"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reactive_observers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- martintomas
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
description: 'Every class or object can be transformed to observer and dynamically
|
70
|
+
react to data changes across several models. Observable module is using build in
|
71
|
+
Active Record hooks or database triggers which can be turned on in multiple App
|
72
|
+
environment. '
|
73
|
+
email:
|
74
|
+
- tomas@jchsoft.cz
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".gitignore"
|
80
|
+
- ".ruby-gemset"
|
81
|
+
- ".ruby-version"
|
82
|
+
- ".travis.yml"
|
83
|
+
- Gemfile
|
84
|
+
- LICENSE.txt
|
85
|
+
- README.md
|
86
|
+
- Rakefile
|
87
|
+
- bin/console
|
88
|
+
- bin/setup
|
89
|
+
- lib/reactive_observers.rb
|
90
|
+
- lib/reactive_observers/base.rb
|
91
|
+
- lib/reactive_observers/configuration.rb
|
92
|
+
- lib/reactive_observers/database_adapters/abstract_adapter.rb
|
93
|
+
- lib/reactive_observers/database_adapters/factory.rb
|
94
|
+
- lib/reactive_observers/database_adapters/postgresql_adapter.rb
|
95
|
+
- lib/reactive_observers/observable/base.rb
|
96
|
+
- lib/reactive_observers/observable/db_listener.rb
|
97
|
+
- lib/reactive_observers/observable/filtering.rb
|
98
|
+
- lib/reactive_observers/observable/notification.rb
|
99
|
+
- lib/reactive_observers/observable/removing.rb
|
100
|
+
- lib/reactive_observers/observer/container.rb
|
101
|
+
- lib/reactive_observers/observer/container_comparator.rb
|
102
|
+
- lib/reactive_observers/observer/container_validator.rb
|
103
|
+
- lib/reactive_observers/version.rb
|
104
|
+
- reactive_observers.gemspec
|
105
|
+
homepage: https://github.com/martintomas/reactive_observers.git
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
metadata:
|
109
|
+
homepage_uri: https://github.com/martintomas/reactive_observers.git
|
110
|
+
source_code_uri: https://github.com/martintomas/reactive_observers.git
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 2.3.0
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.3.1
|
125
|
+
requirements: []
|
126
|
+
rubygems_version: 3.0.4
|
127
|
+
signing_key:
|
128
|
+
specification_version: 4
|
129
|
+
summary: Observe Active Record classes and records reactive way!
|
130
|
+
test_files: []
|