pub_sub_model_sync 0.2.4 → 0.4.2
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 +4 -4
- data/.github/workflows/ruby.yml +24 -7
- data/.rubocop.yml +0 -2
- data/CHANGELOG.md +21 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +13 -9
- data/README.md +65 -33
- data/gemfiles/Gemfile_4 +16 -0
- data/gemfiles/Gemfile_5 +14 -0
- data/gemfiles/Gemfile_6 +14 -0
- data/lib/pub_sub_model_sync.rb +4 -1
- data/lib/pub_sub_model_sync/config.rb +1 -1
- data/lib/pub_sub_model_sync/connector.rb +0 -1
- data/lib/pub_sub_model_sync/message_processor.rb +16 -60
- data/lib/pub_sub_model_sync/message_publisher.rb +33 -0
- data/lib/pub_sub_model_sync/publisher.rb +13 -42
- data/lib/pub_sub_model_sync/publisher_concern.rb +24 -23
- data/lib/pub_sub_model_sync/service_base.rb +3 -3
- data/lib/pub_sub_model_sync/service_google.rb +4 -2
- data/lib/pub_sub_model_sync/service_kafka.rb +2 -2
- data/lib/pub_sub_model_sync/service_rabbit.rb +8 -3
- data/lib/pub_sub_model_sync/subscriber.rb +67 -0
- data/lib/pub_sub_model_sync/subscriber_concern.rb +22 -21
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/pub_sub_model_sync.gemspec +0 -1
- metadata +10 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0bd383488f8214ca409871adf231319b905f41833b8a9db9c2bc58a9d5a2691
|
4
|
+
data.tar.gz: c05ca888a6c085280407f9a1e26fe7552efa1c33f6be7735129ff2f8addd77fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8769aee9d08936fd9fd4707e1dd5664c30ccbb6b2b59e386182ac4a0c173b46813df9ca5ccc3a3e3139af4297c160ca5ddb05a555d8f2c1fbb719e7a54384f4
|
7
|
+
data.tar.gz: bc25309e3894fd5905d0e3fdc180d93a98cfced77ff006f24d0044f1db717f5b0fa475722126901ec18381d3e4890a2a0ff00f7ecb234405cdad4a1e7049a5fb
|
data/.github/workflows/ruby.yml
CHANGED
@@ -6,28 +6,45 @@ on:
|
|
6
6
|
- master
|
7
7
|
pull_request:
|
8
8
|
|
9
|
-
|
10
9
|
jobs:
|
11
10
|
build:
|
12
11
|
name: Tests and Code Style
|
13
12
|
runs-on: ubuntu-latest
|
13
|
+
strategy:
|
14
|
+
matrix:
|
15
|
+
ruby: [2.4, 2.5, 2.6]
|
16
|
+
rails: [4, 5, 6]
|
17
|
+
include:
|
18
|
+
- ruby: 2.7
|
19
|
+
rails: 6
|
20
|
+
exclude: # rails 6 requires ruby >= 2.5
|
21
|
+
- ruby: 2.4
|
22
|
+
rails: 6
|
14
23
|
|
15
24
|
steps:
|
16
25
|
- uses: actions/checkout@v2
|
17
|
-
- name: Set up Ruby
|
26
|
+
- name: Set up Ruby
|
18
27
|
uses: actions/setup-ruby@v1
|
19
28
|
with:
|
20
|
-
ruby-version:
|
21
|
-
|
29
|
+
ruby-version: ${{ matrix.ruby }}
|
22
30
|
- name: Install sqlite3
|
23
31
|
run: sudo apt-get install libsqlite3-dev
|
24
32
|
|
25
|
-
- name:
|
33
|
+
- name: Install bundler
|
34
|
+
env:
|
35
|
+
GEMFILE_PATH: gemfiles/Gemfile_${{ matrix.rails }}
|
36
|
+
RAILS_V: ${{ matrix.rails }}
|
26
37
|
run: |
|
27
|
-
|
28
|
-
|
38
|
+
rm -f Gemfile.lock && rm -f Gemfile
|
39
|
+
cp $GEMFILE_PATH ./Gemfile
|
40
|
+
bundler_v='2.1.4'
|
41
|
+
if [ $RAILS_V = "4" ]; then bundler_v="1.16.6"; fi
|
42
|
+
gem install bundler -v "~> $bundler_v"
|
43
|
+
bundle _${bundler_v}_ install --jobs 4 --retry 3
|
44
|
+
|
29
45
|
- name: Tests (rspec)
|
30
46
|
run: |
|
31
47
|
bundle exec rspec
|
48
|
+
|
32
49
|
- name: Code style (Rubocop)
|
33
50
|
run: bundle exec rubocop
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
# 0.4.1 (May 12, 2020)
|
4
|
+
- chore: improve log messages
|
5
|
+
- feat: do not update model if no changes
|
6
|
+
- feat: skip publisher after updating if no changes
|
7
|
+
|
8
|
+
|
9
|
+
# 0.4.0 (May 06, 2020)
|
10
|
+
- rename as_klass to from_klass and as_action to from_action for subscribers
|
11
|
+
- refactor subscribers to be independent
|
12
|
+
- refactor message_publisher to use publisher
|
13
|
+
- rename publisher into message_publisher
|
14
|
+
- reformat publisher to reuse connector
|
15
|
+
|
16
|
+
# 0.3.1 (May 05, 2020)
|
17
|
+
- improve rabbit service to use sleep instead of block ("Block is not recommended for production")
|
18
|
+
- improve message ID
|
19
|
+
|
20
|
+
# 0.3.0 (April 29, 2020)
|
21
|
+
- Support for multiple identifiers when syncing
|
22
|
+
- Add klass.ps_find_model method for a custom model finder
|
23
|
+
|
3
24
|
# 0.2.4 (April 28, 2020)
|
4
25
|
- Delegate .publish to the .publisher for better understanding
|
5
26
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pub_sub_model_sync (0.2
|
5
|
-
activesupport
|
4
|
+
pub_sub_model_sync (0.4.2)
|
6
5
|
rails
|
7
6
|
|
8
7
|
GEM
|
@@ -72,6 +71,10 @@ GEM
|
|
72
71
|
amq-protocol (~> 2.3, >= 2.3.0)
|
73
72
|
concurrent-ruby (1.1.6)
|
74
73
|
crass (1.0.6)
|
74
|
+
database_cleaner (1.8.4)
|
75
|
+
database_cleaner-active_record (1.8.0)
|
76
|
+
activerecord
|
77
|
+
database_cleaner (~> 1.8.0)
|
75
78
|
diff-lcs (1.3)
|
76
79
|
digest-crc (0.5.1)
|
77
80
|
erubi (1.9.0)
|
@@ -119,7 +122,7 @@ GEM
|
|
119
122
|
concurrent-ruby (~> 1.0)
|
120
123
|
jaro_winkler (1.5.4)
|
121
124
|
jwt (2.2.1)
|
122
|
-
loofah (2.
|
125
|
+
loofah (2.6.0)
|
123
126
|
crass (~> 1.0.2)
|
124
127
|
nokogiri (>= 1.5.9)
|
125
128
|
mail (2.7.1)
|
@@ -128,21 +131,21 @@ GEM
|
|
128
131
|
mimemagic (~> 0.3.2)
|
129
132
|
memoist (0.16.2)
|
130
133
|
method_source (1.0.0)
|
131
|
-
mimemagic (0.3.
|
134
|
+
mimemagic (0.3.5)
|
132
135
|
mini_mime (1.0.2)
|
133
136
|
mini_portile2 (2.4.0)
|
134
137
|
minitest (5.14.0)
|
135
138
|
multi_json (1.14.1)
|
136
139
|
multipart-post (2.1.1)
|
137
140
|
nio4r (2.5.2)
|
138
|
-
nokogiri (1.10.
|
141
|
+
nokogiri (1.10.10)
|
139
142
|
mini_portile2 (~> 2.4.0)
|
140
143
|
os (1.0.1)
|
141
144
|
parallel (1.19.1)
|
142
145
|
parser (2.7.0.4)
|
143
146
|
ast (~> 2.4.0)
|
144
147
|
public_suffix (4.0.3)
|
145
|
-
rack (2.2.
|
148
|
+
rack (2.2.3)
|
146
149
|
rack-test (1.1.0)
|
147
150
|
rack (>= 1.0, < 3)
|
148
151
|
rails (6.0.2.2)
|
@@ -204,7 +207,7 @@ GEM
|
|
204
207
|
faraday (~> 0.9)
|
205
208
|
jwt (>= 1.5, < 3.0)
|
206
209
|
multi_json (~> 1.10)
|
207
|
-
sprockets (4.0.
|
210
|
+
sprockets (4.0.2)
|
208
211
|
concurrent-ruby (~> 1.0)
|
209
212
|
rack (> 1, < 3)
|
210
213
|
sprockets-rails (3.2.1)
|
@@ -217,9 +220,9 @@ GEM
|
|
217
220
|
tzinfo (1.2.7)
|
218
221
|
thread_safe (~> 0.1)
|
219
222
|
unicode-display_width (1.6.1)
|
220
|
-
websocket-driver (0.7.
|
223
|
+
websocket-driver (0.7.3)
|
221
224
|
websocket-extensions (>= 0.1.0)
|
222
|
-
websocket-extensions (0.1.
|
225
|
+
websocket-extensions (0.1.5)
|
223
226
|
zeitwerk (2.3.0)
|
224
227
|
|
225
228
|
PLATFORMS
|
@@ -228,6 +231,7 @@ PLATFORMS
|
|
228
231
|
DEPENDENCIES
|
229
232
|
bundler
|
230
233
|
bunny
|
234
|
+
database_cleaner-active_record
|
231
235
|
google-cloud-pubsub
|
232
236
|
pub_sub_model_sync!
|
233
237
|
rake
|
data/README.md
CHANGED
@@ -61,13 +61,16 @@ And then execute: $ bundle install
|
|
61
61
|
```
|
62
62
|
Note: Publishers do not need todo this
|
63
63
|
|
64
|
+
- Check the service status with:
|
65
|
+
```PubSubModelSync::MessagePublisher.publish_data('Test message', {sample_value: 10}, :create)```
|
66
|
+
|
64
67
|
## Examples
|
65
68
|
```ruby
|
66
69
|
# App 1 (Publisher)
|
67
70
|
# attributes: name email age
|
68
71
|
class User < ActiveRecord::Base
|
69
72
|
include PubSubModelSync::PublisherConcern
|
70
|
-
ps_publish(%i[name email])
|
73
|
+
ps_publish(%i[id name email])
|
71
74
|
end
|
72
75
|
|
73
76
|
# App 2 (Subscriber)
|
@@ -86,7 +89,7 @@ User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to
|
|
86
89
|
User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
|
87
90
|
|
88
91
|
User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
|
89
|
-
PubSubModelSync::
|
92
|
+
PubSubModelSync::MessagePublisher.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
|
90
93
|
```
|
91
94
|
|
92
95
|
## Advanced Example
|
@@ -95,7 +98,7 @@ PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) #
|
|
95
98
|
class User < ActiveRecord::Base
|
96
99
|
self.table_name = 'publisher_users'
|
97
100
|
include PubSubModelSync::PublisherConcern
|
98
|
-
ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client'
|
101
|
+
ps_publish(%i[id:client_id name:full_name email], actions: %i[update], as_klass: 'Client')
|
99
102
|
|
100
103
|
def ps_skip_callback?(_action)
|
101
104
|
false # here logic with action to skip push message
|
@@ -110,30 +113,66 @@ end
|
|
110
113
|
class User < ActiveRecord::Base
|
111
114
|
self.table_name = 'subscriber_users'
|
112
115
|
include PubSubModelSync::SubscriberConcern
|
113
|
-
ps_subscribe(%i[name], actions: %i[update],
|
114
|
-
ps_class_subscribe(:greeting,
|
116
|
+
ps_subscribe(%i[name], actions: %i[update], from_klass: 'Client', id: %i[client_id email])
|
117
|
+
ps_class_subscribe(:greeting, from_action: :custom_greeting, from_klass: 'CustomUser')
|
118
|
+
alias_attribute :full_name, :name
|
115
119
|
|
116
120
|
def self.greeting(data)
|
117
121
|
puts 'Class message called through custom_greeting'
|
118
122
|
end
|
123
|
+
|
124
|
+
# def self.ps_find_model(data)
|
125
|
+
# where(email: data[:email], ...).first_or_initialize
|
126
|
+
# end
|
119
127
|
end
|
120
128
|
```
|
121
129
|
|
122
130
|
Note: Be careful with collision of names
|
123
131
|
```
|
124
|
-
|
125
|
-
|
126
|
-
ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
|
127
|
-
|
128
|
-
def key_data
|
129
|
-
name
|
130
|
-
end
|
131
|
-
end
|
132
|
+
# ps_publish %i[name_data:name name:key] # key will be replaced with name_data
|
133
|
+
ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
|
132
134
|
```
|
133
135
|
|
134
136
|
## API
|
137
|
+
### Subscribers
|
138
|
+
- Permit to configure class level subscriptions
|
139
|
+
```ps_class_subscribe(action_name, from_action: nil, from_klass: nil)```
|
140
|
+
* from_action: (Optional) Source method name
|
141
|
+
* from_klass: (Optional) Source class name
|
142
|
+
|
143
|
+
- Permit to configure instance level subscriptions (CRUD)
|
144
|
+
```ps_subscribe(attrs, from_klass: nil, actions: nil, id: nil)```
|
145
|
+
* attrs: (Array/Required) Array of all attributes to be synced
|
146
|
+
* from_klass: (String/Optional) Source class name (Instead of the model class name, will use this value)
|
147
|
+
* actions: (Array/Optional, default: create/update/destroy) permit to customize action names
|
148
|
+
* id: (Sym|Array/Optional, default: id) Attr identifier(s) to find the corresponding model
|
149
|
+
|
150
|
+
- Permit to configure a custom model finder
|
151
|
+
```ps_find_model(data)```
|
152
|
+
* data: (Hash) Data received from sync
|
153
|
+
Must return an existent or a new model object
|
154
|
+
|
155
|
+
- Get crud subscription configured for the class
|
156
|
+
```User.ps_subscriber(action_name)```
|
157
|
+
* action_name (default :create, :sym): can be :create, :update, :destroy
|
158
|
+
|
159
|
+
- Inspect all configured subscribers
|
160
|
+
```PubSubModelSync::Config.subscribers```
|
161
|
+
|
162
|
+
- Permit to customize the way to detect if the subscribed model was changed (Only for update action).
|
163
|
+
```.ps_subscriber_changed?(data)```
|
164
|
+
By default: ```model.changed?```
|
165
|
+
|
166
|
+
### Publishers
|
167
|
+
- Permit to configure crud publishers
|
168
|
+
```ps_publish(attrs, actions: nil, as_klass: nil)```
|
169
|
+
* attrs: (Array/Required) Array of attributes to be published
|
170
|
+
* actions: (Array/Optional, default: create/update/destroy) permit to customize action names
|
171
|
+
* as_klass: (String/Optional) Output class name (Instead of the model class name, will use this value)
|
172
|
+
|
135
173
|
- Permit to cancel sync called after create/update/destroy (Before initializing sync service)
|
136
174
|
```model.ps_skip_callback?(action)```
|
175
|
+
Default: False
|
137
176
|
Note: Return true to cancel sync
|
138
177
|
|
139
178
|
- Callback called before preparing data for sync (Permit to stop sync)
|
@@ -150,29 +189,24 @@ end
|
|
150
189
|
- Perform sync on demand (:create, :update, :destroy):
|
151
190
|
The target model will receive a notification to perform the indicated action
|
152
191
|
```my_model.ps_perform_sync(action_name, custom_settings = {})```
|
153
|
-
* custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil
|
192
|
+
* custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil })
|
154
193
|
|
155
|
-
-
|
194
|
+
- Publish a class level notification:
|
156
195
|
```User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)```
|
157
196
|
Target class ```User.action_name``` will be called when message is received
|
158
197
|
* data: (required, :hash) message value to deliver
|
159
|
-
* action_name: (required, :sim)
|
160
|
-
* as_klass: (optional, :string)
|
198
|
+
* action_name: (required, :sim) Action name
|
199
|
+
* as_klass: (optional, :string) Custom class name (Default current model name)
|
161
200
|
|
162
|
-
-
|
163
|
-
```PubSubModelSync::
|
201
|
+
- Publish a class level notification (Same as above: on demand call)
|
202
|
+
```PubSubModelSync::MessagePublisher.publish_data(Klass_name, data, action_name)```
|
164
203
|
* klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
|
165
204
|
* data: (required, :hash) message value to deliver
|
166
205
|
* action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
|
167
206
|
|
168
|
-
- Get crud subscription configured for the class
|
169
|
-
```User.ps_subscriber(action_name)```
|
170
|
-
* action_name (default :create, :sym): can be :create, :update, :destroy
|
171
207
|
- Get crud publisher configured for the class
|
172
208
|
```User.ps_publisher(action_name)```
|
173
209
|
* action_name (default :create, :sym): can be :create, :update, :destroy
|
174
|
-
- Inspect all configured listeners
|
175
|
-
```PubSubModelSync::Config.listeners```
|
176
210
|
|
177
211
|
## Testing with RSpec
|
178
212
|
- Config: (spec/rails_helper.rb)
|
@@ -205,12 +239,10 @@ end
|
|
205
239
|
# Subscriber
|
206
240
|
it 'receive model message' do
|
207
241
|
action = :create
|
208
|
-
data = { name: 'name' }
|
209
|
-
|
210
|
-
attrs = PubSubModelSync::Publisher.build_attrs('User', action, user_id)
|
211
|
-
publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action, id: user_id)
|
242
|
+
data = { name: 'name', id: 999 }
|
243
|
+
publisher = PubSubModelSync::MessageProcessor.new(data, 'User', action)
|
212
244
|
publisher.process
|
213
|
-
expect(User.where(id:
|
245
|
+
expect(User.where(id: data[:id]).any?).to be_truth
|
214
246
|
end
|
215
247
|
|
216
248
|
it 'receive class message' do
|
@@ -223,20 +255,20 @@ end
|
|
223
255
|
|
224
256
|
# Publisher
|
225
257
|
it 'publish model action' do
|
226
|
-
publisher = PubSubModelSync::
|
258
|
+
publisher = PubSubModelSync::MessagePublisher
|
227
259
|
data = { name: 'hello'}
|
228
260
|
action = :create
|
229
261
|
User.ps_class_publish(data, action: action)
|
230
262
|
user = User.create(name: 'name', email: 'email')
|
231
|
-
|
263
|
+
expect(publisher).to receive(:publish_model).with(user, :create, anything)
|
232
264
|
end
|
233
265
|
|
234
266
|
it 'publish class message' do
|
235
|
-
publisher = PubSubModelSync::
|
267
|
+
publisher = PubSubModelSync::MessagePublisher
|
236
268
|
data = {msg: 'hello'}
|
237
269
|
action = :greeting
|
238
270
|
User.ps_class_publish(data, action: action)
|
239
|
-
|
271
|
+
expect(publisher).to receive(:publish_data).with('User', data, action)
|
240
272
|
end
|
241
273
|
```
|
242
274
|
|
data/gemfiles/Gemfile_4
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem 'rubocop'
|
4
|
+
gem 'bunny' # rabbit-mq
|
5
|
+
gem 'google-cloud-pubsub' # google pub/sub
|
6
|
+
gem 'ruby-kafka' # kafka pub/sub
|
7
|
+
gem 'rails', '~> 4'
|
8
|
+
gem 'bundler'
|
9
|
+
gem 'sqlite3', '1.3.13'
|
10
|
+
|
11
|
+
group :test do
|
12
|
+
gem 'database_cleaner-active_record'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Specify your gem's dependencies in pub_sub_model_sync.gemspec
|
16
|
+
gemspec
|
data/gemfiles/Gemfile_5
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem 'rubocop'
|
4
|
+
gem 'bunny' # rabbit-mq
|
5
|
+
gem 'google-cloud-pubsub' # google pub/sub
|
6
|
+
gem 'ruby-kafka' # kafka pub/sub
|
7
|
+
gem 'rails', '~> 5'
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'database_cleaner-active_record'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Specify your gem's dependencies in pub_sub_model_sync.gemspec
|
14
|
+
gemspec
|
data/gemfiles/Gemfile_6
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem 'rubocop'
|
4
|
+
gem 'bunny' # rabbit-mq
|
5
|
+
gem 'google-cloud-pubsub' # google pub/sub
|
6
|
+
gem 'ruby-kafka' # kafka pub/sub
|
7
|
+
gem 'rails', '~> 6'
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'database_cleaner-active_record'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Specify your gem's dependencies in pub_sub_model_sync.gemspec
|
14
|
+
gemspec
|
data/lib/pub_sub_model_sync.rb
CHANGED
@@ -6,12 +6,15 @@ require 'active_support'
|
|
6
6
|
require 'pub_sub_model_sync/railtie'
|
7
7
|
require 'pub_sub_model_sync/config'
|
8
8
|
require 'pub_sub_model_sync/subscriber_concern'
|
9
|
-
require 'pub_sub_model_sync/
|
9
|
+
require 'pub_sub_model_sync/message_publisher'
|
10
10
|
require 'pub_sub_model_sync/publisher_concern'
|
11
11
|
require 'pub_sub_model_sync/runner'
|
12
12
|
require 'pub_sub_model_sync/connector'
|
13
13
|
require 'pub_sub_model_sync/message_processor'
|
14
14
|
|
15
|
+
require 'pub_sub_model_sync/publisher'
|
16
|
+
require 'pub_sub_model_sync/subscriber'
|
17
|
+
|
15
18
|
require 'pub_sub_model_sync/service_base'
|
16
19
|
require 'pub_sub_model_sync/service_google'
|
17
20
|
require 'pub_sub_model_sync/service_rabbit'
|
@@ -2,83 +2,39 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class MessageProcessor
|
5
|
-
attr_accessor :data, :
|
5
|
+
attr_accessor :data, :klass, :action
|
6
6
|
|
7
7
|
# @param data (Hash): any hash value to deliver
|
8
|
-
|
9
|
-
def initialize(data, klass, action, settings = {})
|
8
|
+
def initialize(data, klass, action)
|
10
9
|
@data = data
|
11
|
-
@
|
12
|
-
@
|
10
|
+
@klass = klass
|
11
|
+
@action = action
|
13
12
|
end
|
14
13
|
|
15
14
|
def process
|
16
|
-
|
17
|
-
|
18
|
-
listeners = filter_listeners
|
19
|
-
return log 'Skipped: No listeners' unless listeners.any?
|
20
|
-
|
21
|
-
eval_message(listeners)
|
22
|
-
log 'processed message' unless @failed
|
15
|
+
subscribers = filter_subscribers
|
16
|
+
subscribers.each { |subscriber| run_subscriber(subscriber) }
|
23
17
|
end
|
24
18
|
|
25
19
|
private
|
26
20
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
call_class_listener(listener)
|
31
|
-
else
|
32
|
-
call_listener(listener)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def call_class_listener(listener)
|
38
|
-
model_class = listener[:klass].constantize
|
39
|
-
model_class.send(listener[:action], data)
|
21
|
+
def run_subscriber(subscriber)
|
22
|
+
subscriber.eval_message(data)
|
23
|
+
log "processed message with: #{[klass, action, data]}"
|
40
24
|
rescue => e
|
41
|
-
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
# support for: create, update, destroy
|
46
|
-
def call_listener(listener)
|
47
|
-
model = find_model(listener)
|
48
|
-
if attrs[:action].to_sym == :destroy
|
49
|
-
model.destroy!
|
50
|
-
else
|
51
|
-
populate_model(model, listener)
|
52
|
-
model.save!
|
53
|
-
end
|
54
|
-
rescue => e
|
55
|
-
log("Error listener (#{listener}): #{e.message}", :error)
|
56
|
-
@failed = true
|
57
|
-
end
|
58
|
-
|
59
|
-
def find_model(listener)
|
60
|
-
model_class = listener[:klass].constantize
|
61
|
-
identifier = listener[:settings][:id] || :id
|
62
|
-
model_class.where(identifier => attrs[:id]).first_or_initialize
|
63
|
-
end
|
64
|
-
|
65
|
-
def populate_model(model, listener)
|
66
|
-
values = data.slice(*listener[:settings][:attrs])
|
67
|
-
values.each do |attr, value|
|
68
|
-
model.send("#{attr}=", value)
|
69
|
-
end
|
25
|
+
info = [klass, action, data, e.message, e.backtrace]
|
26
|
+
log("error processing message: #{info}", :error)
|
70
27
|
end
|
71
28
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
listener[:as_action].to_s == attrs[:action].to_s
|
29
|
+
def filter_subscribers
|
30
|
+
PubSubModelSync::Config.subscribers.select do |subscriber|
|
31
|
+
subscriber.settings[:from_klass].to_s == klass.to_s &&
|
32
|
+
subscriber.settings[:from_action].to_s == action.to_s
|
77
33
|
end
|
78
34
|
end
|
79
35
|
|
80
36
|
def log(message, kind = :info)
|
81
|
-
PubSubModelSync::Config.log
|
37
|
+
PubSubModelSync::Config.log message, kind
|
82
38
|
end
|
83
39
|
end
|
84
40
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class MessagePublisher
|
5
|
+
class << self
|
6
|
+
delegate :publish, to: :connector
|
7
|
+
|
8
|
+
def connector
|
9
|
+
@connector ||= PubSubModelSync::Connector.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish_data(klass, data, action)
|
13
|
+
attrs = { klass: klass.to_s, action: action.to_sym }
|
14
|
+
publish(data, attrs)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param model: ActiveRecord model
|
18
|
+
# @param action: (Sym) Action name
|
19
|
+
# @param publisher: (Publisher, optional) Publisher to be used
|
20
|
+
def publish_model(model, action, publisher = nil)
|
21
|
+
return if model.ps_skip_sync?(action)
|
22
|
+
|
23
|
+
publisher ||= model.class.ps_publisher(action)
|
24
|
+
payload = publisher.payload(model, action)
|
25
|
+
res_before = model.ps_before_sync(action, payload[:data])
|
26
|
+
return if res_before == :cancel
|
27
|
+
|
28
|
+
publish(payload[:data], payload[:attrs])
|
29
|
+
model.ps_after_sync(action, payload[:data])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -2,47 +2,24 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Publisher
|
5
|
-
attr_accessor :
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@
|
10
|
-
|
11
|
-
|
12
|
-
def publish_data(klass, data, action)
|
13
|
-
attributes = self.class.build_attrs(klass, action)
|
14
|
-
publish(data, attributes)
|
5
|
+
attr_accessor :attrs, :actions, :klass, :as_klass
|
6
|
+
def initialize(attrs, klass, actions = nil, as_klass = nil)
|
7
|
+
@attrs = attrs
|
8
|
+
@klass = klass
|
9
|
+
@actions = actions || %i[create update destroy]
|
10
|
+
@as_klass = as_klass || klass
|
15
11
|
end
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
return if model.ps_skip_sync?(action)
|
20
|
-
|
21
|
-
settings ||= model.class.ps_publisher_info(action)
|
22
|
-
attributes = build_model_attrs(model, action, settings)
|
23
|
-
data = {}
|
24
|
-
data = build_model_data(model, settings[:attrs]) if action != :destroy
|
25
|
-
res_before = model.ps_before_sync(action, data)
|
26
|
-
return if res_before == :cancel
|
27
|
-
|
28
|
-
publish(data.symbolize_keys, attributes)
|
29
|
-
model.ps_after_sync(action, data)
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.build_attrs(klass, action, id = nil)
|
33
|
-
{
|
34
|
-
klass: klass.to_s,
|
35
|
-
action: action.to_sym,
|
36
|
-
id: id
|
37
|
-
}
|
13
|
+
def payload(model, action)
|
14
|
+
{ data: payload_data(model), attrs: payload_attrs(model, action) }
|
38
15
|
end
|
39
16
|
|
40
17
|
private
|
41
18
|
|
42
|
-
def
|
43
|
-
source_props =
|
19
|
+
def payload_data(model)
|
20
|
+
source_props = @attrs.map { |prop| prop.to_s.split(':').first }
|
44
21
|
data = model.as_json(only: source_props, methods: source_props)
|
45
|
-
aliased_props =
|
22
|
+
aliased_props = @attrs.select { |prop| prop.to_s.include?(':') }
|
46
23
|
aliased_props.each do |prop|
|
47
24
|
source, target = prop.to_s.split(':')
|
48
25
|
data[target] = data.delete(source)
|
@@ -50,14 +27,8 @@ module PubSubModelSync
|
|
50
27
|
data.symbolize_keys
|
51
28
|
end
|
52
29
|
|
53
|
-
def
|
54
|
-
|
55
|
-
id_val = model.send(settings[:id] || :id)
|
56
|
-
self.class.build_attrs(as_klass, action, id_val)
|
57
|
-
end
|
58
|
-
|
59
|
-
def log(msg)
|
60
|
-
PubSubModelSync::Config.log(msg)
|
30
|
+
def payload_attrs(model, action)
|
31
|
+
{ klass: (as_klass || model.class.name).to_s, action: action.to_sym }
|
61
32
|
end
|
62
33
|
end
|
63
34
|
end
|
@@ -11,6 +11,7 @@ module PubSubModelSync
|
|
11
11
|
false
|
12
12
|
end
|
13
13
|
|
14
|
+
# TODO: make it using respond_to?(:ps_skip_sync?)
|
14
15
|
# before preparing data to sync
|
15
16
|
def ps_skip_sync?(_action)
|
16
17
|
false
|
@@ -23,49 +24,49 @@ module PubSubModelSync
|
|
23
24
|
def ps_after_sync(_action, _data); end
|
24
25
|
|
25
26
|
# To perform sync on demand
|
26
|
-
# @param
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
# @param attrs (Array, optional): custom attrs to be used
|
28
|
+
# @param as_klass (Array, optional): custom klass name to be used
|
29
|
+
# @param publisher (Publisher, optional): custom publisher object
|
30
|
+
def ps_perform_sync(action = :create, attrs: nil, as_klass: nil,
|
31
|
+
publisher: nil)
|
32
|
+
publisher ||= self.class.ps_publisher(action).dup
|
33
|
+
publisher.attrs = attrs if attrs
|
34
|
+
publisher.as_klass = as_klass if as_klass
|
35
|
+
PubSubModelSync::MessagePublisher.publish_model(self, action, publisher)
|
31
36
|
end
|
32
37
|
|
33
38
|
module ClassMethods
|
34
39
|
# Permit to configure to publish crud actions (:create, :update, :destroy)
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
def ps_publish(attrs, actions: %i[create update destroy], as_klass: nil)
|
41
|
+
klass = PubSubModelSync::Publisher
|
42
|
+
publisher = klass.new(attrs, name, actions, as_klass)
|
43
|
+
PubSubModelSync::Config.publishers << publisher
|
38
44
|
actions.each do |action|
|
39
|
-
|
40
|
-
PubSubModelSync::Config.publishers << info
|
41
|
-
ps_register_callback(action.to_sym, info)
|
45
|
+
ps_register_callback(action.to_sym, publisher)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
49
|
# On demand class level publisher
|
46
50
|
def ps_class_publish(data, action:, as_klass: nil)
|
47
51
|
as_klass = (as_klass || name).to_s
|
48
|
-
|
52
|
+
klass = PubSubModelSync::MessagePublisher
|
53
|
+
klass.publish_data(as_klass, data, action.to_sym)
|
49
54
|
end
|
50
55
|
|
51
56
|
# Publisher info for specific action
|
52
|
-
def
|
53
|
-
PubSubModelSync::Config.publishers.
|
54
|
-
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def ps_publisher_service
|
59
|
-
PubSubModelSync::Publisher.new
|
57
|
+
def ps_publisher(action = :create)
|
58
|
+
PubSubModelSync::Config.publishers.find do |publisher|
|
59
|
+
publisher.klass == name && publisher.actions.include?(action)
|
60
|
+
end
|
60
61
|
end
|
61
62
|
|
62
63
|
private
|
63
64
|
|
64
|
-
def ps_register_callback(action,
|
65
|
+
def ps_register_callback(action, publisher)
|
65
66
|
after_commit(on: action) do |model|
|
66
67
|
unless model.ps_skip_callback?(action)
|
67
|
-
|
68
|
-
|
68
|
+
klass = PubSubModelSync::MessagePublisher
|
69
|
+
klass.publish_model(model, action.to_sym, publisher)
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
@@ -18,11 +18,11 @@ module PubSubModelSync
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
# @param payload (String JSON): '{"data":{},"attributes":{..}}'
|
22
|
-
# refer: PubSubModelSync::
|
21
|
+
# @param payload (String JSON): '{"data":{}, "attributes":{..}}'
|
22
|
+
# refer: PubSubModelSync::MessagePublisher(.publish_model | .publish_data)
|
23
23
|
def perform_message(payload)
|
24
24
|
data, attrs = parse_message_payload(payload)
|
25
|
-
args = [data, attrs[:klass], attrs[:action]
|
25
|
+
args = [data, attrs[:klass], attrs[:action]]
|
26
26
|
PubSubModelSync::MessageProcessor.new(*args).process
|
27
27
|
end
|
28
28
|
|
@@ -29,10 +29,12 @@ module PubSubModelSync
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def publish(data, attributes)
|
32
|
-
log("Publishing message: #{[
|
33
|
-
|
32
|
+
log("Publishing message: #{[attributes, data]}")
|
34
33
|
payload = { data: data, attributes: attributes }.to_json
|
35
34
|
topic.publish(payload, { SERVICE_KEY => true })
|
35
|
+
rescue => e
|
36
|
+
info = [attributes, data, e.message, e.backtrace]
|
37
|
+
log("Error publishing: #{info}", :error)
|
36
38
|
end
|
37
39
|
|
38
40
|
def stop
|
@@ -29,12 +29,12 @@ module PubSubModelSync
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def publish(data, attributes)
|
32
|
-
log("Publishing: #{[
|
32
|
+
log("Publishing: #{[attributes, data]}")
|
33
33
|
payload = { data: data, attributes: attributes }
|
34
34
|
producer.produce(payload.to_json, message_settings)
|
35
35
|
producer.deliver_messages
|
36
36
|
rescue => e
|
37
|
-
info = [
|
37
|
+
info = [attributes, data, e.message, e.backtrace]
|
38
38
|
log("Error publishing: #{info}", :error)
|
39
39
|
end
|
40
40
|
|
@@ -19,7 +19,8 @@ module PubSubModelSync
|
|
19
19
|
log('Listener starting...')
|
20
20
|
subscribe_to_queue
|
21
21
|
log('Listener started')
|
22
|
-
queue.subscribe(
|
22
|
+
queue.subscribe(subscribe_settings, &method(:process_message))
|
23
|
+
loop { sleep 5 }
|
23
24
|
rescue PubSubModelSync::Runner::ShutDown
|
24
25
|
raise
|
25
26
|
rescue => e
|
@@ -27,7 +28,7 @@ module PubSubModelSync
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def publish(data, attributes)
|
30
|
-
log("Publishing: #{[
|
31
|
+
log("Publishing: #{[attributes, data]}")
|
31
32
|
deliver_data(data, attributes)
|
32
33
|
# TODO: max retry
|
33
34
|
rescue Timeout::Error => e
|
@@ -35,7 +36,7 @@ module PubSubModelSync
|
|
35
36
|
initialize
|
36
37
|
retry
|
37
38
|
rescue => e
|
38
|
-
info = [
|
39
|
+
info = [attributes, data, e.message, e.backtrace]
|
39
40
|
log("Error publishing: #{info}", :error)
|
40
41
|
end
|
41
42
|
|
@@ -50,6 +51,10 @@ module PubSubModelSync
|
|
50
51
|
{ routing_key: queue.name, type: SERVICE_KEY }
|
51
52
|
end
|
52
53
|
|
54
|
+
def subscribe_settings
|
55
|
+
{ manual_ack: false }
|
56
|
+
end
|
57
|
+
|
53
58
|
def process_message(_delivery_info, meta_info, payload)
|
54
59
|
return unless meta_info[:type] == SERVICE_KEY
|
55
60
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class Subscriber
|
5
|
+
attr_accessor :klass, :action, :attrs, :settings
|
6
|
+
|
7
|
+
# @param settings: (Hash) { id: :id, direct_mode: false,
|
8
|
+
# from_klass: klass, from_action: action }
|
9
|
+
def initialize(klass, action, attrs: nil, settings: {})
|
10
|
+
def_settings = { id: :id, direct_mode: false,
|
11
|
+
from_klass: klass, from_action: action }
|
12
|
+
@klass = klass
|
13
|
+
@action = action
|
14
|
+
@attrs = attrs
|
15
|
+
@settings = def_settings.merge(settings)
|
16
|
+
end
|
17
|
+
|
18
|
+
def eval_message(message)
|
19
|
+
if settings[:direct_mode]
|
20
|
+
run_class_message(message)
|
21
|
+
else
|
22
|
+
run_model_message(message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def run_class_message(message)
|
29
|
+
model_class = klass.constantize
|
30
|
+
model_class.send(action, message)
|
31
|
+
end
|
32
|
+
|
33
|
+
# support for: create, update, destroy
|
34
|
+
def run_model_message(message)
|
35
|
+
model = find_model(message)
|
36
|
+
if action == :destroy
|
37
|
+
model.destroy!
|
38
|
+
else
|
39
|
+
populate_model(model, message)
|
40
|
+
return if action == :update && !model.ps_subscriber_changed?(message)
|
41
|
+
|
42
|
+
model.save!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_model(message)
|
47
|
+
model_class = klass.constantize
|
48
|
+
if model_class.respond_to?(:ps_find_model)
|
49
|
+
return model_class.ps_find_model(message)
|
50
|
+
end
|
51
|
+
|
52
|
+
model_class.where(model_identifiers(message)).first_or_initialize
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_identifiers(message)
|
56
|
+
identifiers = Array(settings[:id])
|
57
|
+
identifiers.map { |key| [key, message[key.to_sym]] }.to_h
|
58
|
+
end
|
59
|
+
|
60
|
+
def populate_model(model, message)
|
61
|
+
values = message.slice(*attrs)
|
62
|
+
values.each do |attr, value|
|
63
|
+
model.send("#{attr}=", value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -6,39 +6,40 @@ module PubSubModelSync
|
|
6
6
|
base.extend(ClassMethods)
|
7
7
|
end
|
8
8
|
|
9
|
+
# check if model was changed to .save!
|
10
|
+
def ps_subscriber_changed?(_data)
|
11
|
+
changed?
|
12
|
+
end
|
13
|
+
|
9
14
|
module ClassMethods
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
actions = settings.delete(:actions) || %i[create update destroy]
|
14
|
-
settings = settings.merge(attrs: attrs)
|
15
|
+
def ps_subscribe(attrs, actions: nil, from_klass: name, id: :id)
|
16
|
+
settings = { id: id, from_klass: from_klass }
|
17
|
+
actions ||= %i[create update destroy]
|
15
18
|
actions.each do |action|
|
16
|
-
add_ps_subscriber(
|
19
|
+
add_ps_subscriber(action, attrs, settings)
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
def ps_class_subscribe(action,
|
21
|
-
|
23
|
+
def ps_class_subscribe(action, from_action: nil, from_klass: nil)
|
24
|
+
settings = { direct_mode: true }
|
25
|
+
settings[:from_action] = from_action if from_action
|
26
|
+
settings[:from_klass] = from_klass if from_klass
|
27
|
+
add_ps_subscriber(action, nil, settings)
|
22
28
|
end
|
23
29
|
|
24
30
|
def ps_subscriber(action = :create)
|
25
|
-
PubSubModelSync::Config.
|
26
|
-
|
27
|
-
end
|
31
|
+
PubSubModelSync::Config.subscribers.find do |subscriber|
|
32
|
+
subscriber.klass == name && subscriber.action == action
|
33
|
+
end
|
28
34
|
end
|
29
35
|
|
30
36
|
private
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
as_action: (as_action || action).to_sym,
|
38
|
-
direct_mode: direct_mode,
|
39
|
-
settings: settings
|
40
|
-
}
|
41
|
-
PubSubModelSync::Config.listeners << listener
|
38
|
+
# @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
|
39
|
+
def add_ps_subscriber(action, attrs, settings = {})
|
40
|
+
klass = PubSubModelSync::Subscriber
|
41
|
+
subscriber = klass.new(name, action, attrs: attrs, settings: settings)
|
42
|
+
PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
|
42
43
|
end
|
43
44
|
end
|
44
45
|
end
|
data/pub_sub_model_sync.gemspec
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pub_sub_model_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Owen
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: activesupport
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rails
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,10 +100,14 @@ files:
|
|
114
100
|
- Rakefile
|
115
101
|
- bin/console
|
116
102
|
- bin/setup
|
103
|
+
- gemfiles/Gemfile_4
|
104
|
+
- gemfiles/Gemfile_5
|
105
|
+
- gemfiles/Gemfile_6
|
117
106
|
- lib/pub_sub_model_sync.rb
|
118
107
|
- lib/pub_sub_model_sync/config.rb
|
119
108
|
- lib/pub_sub_model_sync/connector.rb
|
120
109
|
- lib/pub_sub_model_sync/message_processor.rb
|
110
|
+
- lib/pub_sub_model_sync/message_publisher.rb
|
121
111
|
- lib/pub_sub_model_sync/mock_google_service.rb
|
122
112
|
- lib/pub_sub_model_sync/mock_kafka_service.rb
|
123
113
|
- lib/pub_sub_model_sync/mock_rabbit_service.rb
|
@@ -129,6 +119,7 @@ files:
|
|
129
119
|
- lib/pub_sub_model_sync/service_google.rb
|
130
120
|
- lib/pub_sub_model_sync/service_kafka.rb
|
131
121
|
- lib/pub_sub_model_sync/service_rabbit.rb
|
122
|
+
- lib/pub_sub_model_sync/subscriber.rb
|
132
123
|
- lib/pub_sub_model_sync/subscriber_concern.rb
|
133
124
|
- lib/pub_sub_model_sync/tasks/worker.rake
|
134
125
|
- lib/pub_sub_model_sync/version.rb
|
@@ -140,7 +131,7 @@ metadata:
|
|
140
131
|
homepage_uri: https://github.com/owen2345/pub_sub_model_sync
|
141
132
|
source_code_uri: https://github.com/owen2345/pub_sub_model_sync
|
142
133
|
changelog_uri: https://github.com/owen2345/pub_sub_model_sync/blob/master/CHANGELOG.md
|
143
|
-
post_install_message:
|
134
|
+
post_install_message:
|
144
135
|
rdoc_options: []
|
145
136
|
require_paths:
|
146
137
|
- lib
|
@@ -156,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
147
|
version: '0'
|
157
148
|
requirements: []
|
158
149
|
rubygems_version: 3.0.8
|
159
|
-
signing_key:
|
150
|
+
signing_key:
|
160
151
|
specification_version: 4
|
161
152
|
summary: Permit to sync models between apps through pub/sub
|
162
153
|
test_files: []
|