pub_sub_model_sync 0.1.0 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0738657d975fdfaa12d2f28bfa942674faacd737e9be11435718204d3d90f3b
4
- data.tar.gz: 234400d0857ad54f4498d531db75557e641b08dc3d237c37122cad07ecfa4d92
3
+ metadata.gz: fd18a29b1e99da881b9c362dbc5541f6aeee0211988bcf301336691ac071e96d
4
+ data.tar.gz: cbc025ff0301e1bf4fd35d78fa2b70f06ff8a3c7c1dbfa12448f1cd3a53e592f
5
5
  SHA512:
6
- metadata.gz: 92a83128b548aa1b0b495ad0c67617d30c62b4566e154609009b089d136d439e40b8a0d0c7fe8b48a13aa940f232129eca5c3c20bec4f3d3288411a4757d6ca3
7
- data.tar.gz: 319cc08d44633713db44eae121fda9187536f4b34abdfa8a030fa519f6e48b9aa2bf9c43542b02c133eb095e95a42b03e9d8fd5b62100ee063264fecbb13c5b8
6
+ metadata.gz: 3036baf17c9f78c6b4b445eb5a3a2f7be63c2a78cb36dcf9aad1abcf1eff8b691979cd1ff79e1bac916b44b1996e0f80bedf841be102ad1ec29684c1437910a2
7
+ data.tar.gz: aead45d302ed53d84457376dc9fa64f05d267b7441125722d4621d4d2fc9d7097fa24f3aa693c50862b616f5a25b32e77a202a0503143d347f80290728701680
@@ -4,7 +4,7 @@ language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
6
  - 2.3.1
7
- before_install: gem install bundler -v 2.0.2
7
+ before_install: gem install bundler
8
8
 
9
9
  script:
10
10
  - bundle install
@@ -0,0 +1,23 @@
1
+ # Change Log
2
+
3
+ # 0.1.5
4
+ - Add apache kafka support
5
+ - Add Service interface for future references
6
+ - Improve Services to use a single/common message performer
7
+
8
+ # 0.1.4
9
+ - Add attribute aliases when publishing, ```ps_publish(['name:full_name', 'email'])```
10
+ - Ability to retrieve publisher/subscriber crud settings
11
+
12
+ # 0.1.3
13
+ - shorter publisher/subscriber methods: ps_msync_subscribe into ps_subscribe
14
+
15
+ # 0.1.2
16
+ - fix not found pub/sub library (buggy)
17
+
18
+ # 0.1.1
19
+ - Add rabbitmq pub/sub service support
20
+ - Reformat to support multiple pub/sub services
21
+
22
+ # 0.1.0
23
+ - Google pub/sub support
data/Gemfile CHANGED
@@ -1,6 +1,9 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem 'rubocop', '~> 0.80.1', require: false
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
4
7
 
5
8
  # Specify your gem's dependencies in pub_sub_model_sync.gemspec
6
9
  gemspec
@@ -1,9 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (0.1.0)
4
+ pub_sub_model_sync (0.1.5)
5
5
  activesupport
6
- google-cloud-pubsub
7
6
  rails
8
7
 
9
8
  GEM
@@ -52,12 +51,16 @@ GEM
52
51
  tzinfo (~> 1.1)
53
52
  addressable (2.7.0)
54
53
  public_suffix (>= 2.0.2, < 5.0)
54
+ amq-protocol (2.3.0)
55
55
  arel (9.0.0)
56
56
  ast (2.4.0)
57
57
  builder (3.2.4)
58
+ bunny (2.14.3)
59
+ amq-protocol (~> 2.3, >= 2.3.0)
58
60
  concurrent-ruby (1.1.6)
59
61
  crass (1.0.6)
60
62
  diff-lcs (1.3)
63
+ digest-crc (0.5.1)
61
64
  erubi (1.9.0)
62
65
  faraday (0.17.3)
63
66
  multipart-post (>= 1.2, < 3)
@@ -178,6 +181,8 @@ GEM
178
181
  rexml
179
182
  ruby-progressbar (~> 1.7)
180
183
  unicode-display_width (>= 1.4.0, < 1.7)
184
+ ruby-kafka (1.0.0)
185
+ digest-crc
181
186
  ruby-progressbar (1.10.1)
182
187
  signet (0.11.0)
183
188
  addressable (~> 2.3)
@@ -206,10 +211,13 @@ PLATFORMS
206
211
 
207
212
  DEPENDENCIES
208
213
  bundler
214
+ bunny
215
+ google-cloud-pubsub
209
216
  pub_sub_model_sync!
210
217
  rake
211
218
  rspec
212
- rubocop (~> 0.80.1)
219
+ rubocop
220
+ ruby-kafka
213
221
  sqlite3
214
222
 
215
223
  BUNDLED WITH
data/README.md CHANGED
@@ -1,55 +1,80 @@
1
1
  # PubSubModelSync
2
- Permit to sync models between rails apps through google (Proximately RabbitMQ) pub/sub service.
2
+ Permit to sync models data and make calls between rails apps using google or rabbitmq or apache kafka pub/sub service.
3
+
3
4
  Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) which for now looks unmaintained.
4
5
 
5
- # Features
6
- - Sync CRUD operation between Rails apps. So, all changes made on App1, will be reflected on App2.
6
+ ## Features
7
+ - Sync CRUD operations between Rails apps. So, all changes made on App1, will be reflected on App2, App3.
7
8
  Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
8
- - Ability to make class level communication
9
- Example: If User from App1 wants to generate_email, this can be listened on App2 to make corresponding actions
9
+ - Ability to make class level communication
10
+ Example: If User from App1 wants to generate_email, this can be listened on App2, App3, ... to make corresponding actions
11
+ - Change pub/sub service at any time
10
12
 
11
13
  ## Installation
12
14
  Add this line to your application's Gemfile:
13
15
  ```ruby
14
16
  gem 'pub_sub_model_sync'
17
+
18
+ gem 'google-cloud-pubsub' # to use google pub/sub service
19
+ gem 'bunny' # to use rabbit-mq pub/sub service
20
+ gem 'ruby-kafka' # to use apache kafka pub/sub service
15
21
  ```
16
22
  And then execute: $ bundle install
17
23
 
18
24
 
19
25
  ## Usage
20
26
 
21
- - Configure pub/sub service (Google pub/sub)
27
+ - Configuration for google pub/sub (You need google pub/sub service account)
22
28
  ```ruby
23
- # initializers/pub_sub_config.rb
24
- PubSubModelSync::Config.project = ''
25
- PubSubModelSync::Config.credentials = ''
26
- PubSubModelSync::Config.topic_name = ''
27
- PubSubModelSync::Config.subscription_name = ''
29
+ # initializers/pub_sub_config.rb
30
+ PubSubModelSync::Config.service_name = :google
31
+ PubSubModelSync::Config.project = 'project-id'
32
+ PubSubModelSync::Config.credentials = 'path-to-the-config'
33
+ PubSubModelSync::Config.topic_name = 'sample-topic'
34
+ PubSubModelSync::Config.subscription_name = 'p1-subscriber'
28
35
  ```
29
36
  See details here:
30
37
  https://github.com/googleapis/google-cloud-ruby/tree/master/google-cloud-pubsub
31
38
 
39
+ - configuration for RabbitMq (You need rabbitmq installed)
40
+ ```ruby
41
+ PubSubModelSync::Config.service_name = :rabbitmq
42
+ PubSubModelSync::Config.bunny_connection = 'amqp://guest:guest@localhost'
43
+ PubSubModelSync::Config.queue_name = ''
44
+ PubSubModelSync::Config.topic_name = 'sample-topic'
45
+ ```
46
+ See details here: https://github.com/ruby-amqp/bunny
47
+
48
+ - configuration for Apache Kafka (You need kafka installed)
49
+ ```ruby
50
+ PubSubModelSync::Config.service_name = :kafka
51
+ PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "kafka2:9092"], logger: Rails.logger]
52
+ PubSubModelSync::Config.topic_name = 'sample-topic'
53
+ ```
54
+ See details here: https://github.com/zendesk/ruby-kafka
55
+
32
56
  - Add publishers/subscribers to your models (See examples below)
33
57
 
34
- - Start listening for publishers (Only if the app has subscribers)
58
+ - Start subscribers to listen for publishers (Only in the app that has subscribers)
35
59
  ```ruby
36
60
  rake pub_sub_model_sync:start
37
61
  ```
62
+ Note: Publishers do not need todo this
38
63
 
39
64
  ## Examples
40
65
  ```ruby
41
- # App 1
66
+ # App 1 (Publisher)
42
67
  # attributes: name email age
43
68
  class User < ActiveRecord::Base
44
69
  include PubSubModelSync::PublisherConcern
45
- ps_msync_publish(%i[name email])
70
+ ps_publish(%i[name email])
46
71
  end
47
72
 
48
- # App 2
73
+ # App 2 (Subscriber)
49
74
  class User < ActiveRecord::Base
50
75
  include PubSubModelSync::SubscriberConcern
51
- ps_msync_subscribe(%i[name])
52
- ps_msync_class_subscribe(:greeting)
76
+ ps_subscribe(%i[name])
77
+ ps_class_subscribe(:greeting)
53
78
 
54
79
  def self.greeting(data)
55
80
  puts 'Class message called'
@@ -57,29 +82,30 @@ class User < ActiveRecord::Base
57
82
  end
58
83
 
59
84
  # Samples
60
- User.create(name: 'test user') # Review your App 2 to see the created user (only name will be saved)
61
- User.ps_msync_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
85
+ User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to see the created user (only name will be saved)
86
+ User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
87
+ PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
62
88
  ```
63
89
 
64
90
  ## Advanced Example
65
91
  ```ruby
66
- # App 1
92
+ # App 1 (Publisher)
67
93
  class User < ActiveRecord::Base
68
94
  self.table_name = 'publisher_users'
69
95
  include PubSubModelSync::PublisherConcern
70
- ps_msync_publish(%i[name], actions: %i[update], as_klass: 'Client', id: :client_id)
96
+ ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client', id: :client_id)
71
97
 
72
- def ps_msync_skip_for?(_action)
98
+ def ps_skip_for?(_action)
73
99
  false # here logic with action to skip push message
74
100
  end
75
101
  end
76
102
 
77
- # App 2
103
+ # App 2 (Subscriber)
78
104
  class User < ActiveRecord::Base
79
105
  self.table_name = 'subscriber_users'
80
106
  include PubSubModelSync::SubscriberConcern
81
- ps_msync_subscribe(%i[name], actions: %i[update], as_klass: 'Client', id: :custom_id)
82
- ps_msync_class_subscribe(:greeting, as_action: :custom_greeting, as_klass: 'CustomUser')
107
+ ps_subscribe(%i[name], actions: %i[update], as_klass: 'Client', id: :custom_id)
108
+ ps_class_subscribe(:greeting, as_action: :custom_greeting, as_klass: 'CustomUser')
83
109
 
84
110
  def self.greeting(data)
85
111
  puts 'Class message called through custom_greeting'
@@ -87,16 +113,31 @@ class User < ActiveRecord::Base
87
113
  end
88
114
  ```
89
115
 
90
- ## Testing
91
- - Rspec:
116
+ ## Testing with RSpec
117
+ - Config: (spec/rails_helper.rb)
92
118
  ```ruby
93
- # mock google service
94
- # rails_helper.rb
119
+
120
+ # when using google service
95
121
  require 'pub_sub_model_sync/mock_google_service'
96
122
  config.before(:each) do
97
- pub_sub_mock = PubSubModelSync::MockGoogleService.new
98
- allow(Google::Cloud::Pubsub).to receive(:new).and_return(pub_sub_mock)
123
+ google_mock = PubSubModelSync::MockGoogleService.new
124
+ allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
125
+ end
126
+
127
+ # when using rabbitmq service
128
+ require 'pub_sub_model_sync/mock_rabbit_service'
129
+ config.before(:each) do
130
+ rabbit_mock = PubSubModelSync::MockRabbitService.new
131
+ allow(Bunny).to receive(:new).and_return(rabbit_mock)
99
132
  end
133
+
134
+ # when using apache kafka service
135
+ require 'pub_sub_model_sync/mock_kafka_service'
136
+ config.before(:each) do
137
+ kafka_mock = PubSubModelSync::MockKafkaService.new
138
+ allow(Kafka).to receive(:new).and_return(kafka_mock)
139
+ end
140
+
100
141
  ```
101
142
  - Examples:
102
143
  ```ruby
@@ -124,7 +165,7 @@ end
124
165
  publisher = PubSubModelSync::Publisher
125
166
  data = { name: 'hello'}
126
167
  action = :create
127
- User.ps_msync_class_publish(data, action: action)
168
+ User.ps_class_publish(data, action: action)
128
169
  user = User.create(name: 'name', email: 'email')
129
170
  expect_any_instance_of(publisher).to receive(:publish_model).with(user, :create, anything)
130
171
  end
@@ -133,23 +174,23 @@ end
133
174
  publisher = PubSubModelSync::Publisher
134
175
  data = {msg: 'hello'}
135
176
  action = :greeting
136
- User.ps_msync_class_publish(data, action: action)
177
+ User.ps_class_publish(data, action: action)
137
178
  expect_any_instance_of(publisher).to receive(:publish_data).with('User', data, action)
138
179
  end
139
180
  ```
140
181
 
141
- There are two special methods to extract configured crud settings (attrs, id, ...):
182
+ There are two special methods to extract crud configuration settings (attrs, id, ...):
142
183
 
143
- Subscribers: ```User.ps_msync_subscriber_settings```
184
+ Subscribers: ```User.ps_subscriber```
144
185
 
145
- Publishers: ```User.ps_msync_publisher_settings```
186
+ Publishers: ```User.ps_publisher```
146
187
 
147
188
  Note: Inspect all configured listeners with:
148
189
  ``` PubSubModelSync::Config.listeners ```
149
190
 
150
191
  ## Contributing
151
192
 
152
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pub_sub_model_sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
193
+ Bug reports and pull requests are welcome on GitHub at https://github.com/owen2345/pub_sub_model_sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
153
194
 
154
195
  ## License
155
196
 
@@ -12,6 +12,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/service_base'
16
+ require 'pub_sub_model_sync/service_google'
17
+ require 'pub_sub_model_sync/service_rabbit'
18
+ require 'pub_sub_model_sync/service_kafka'
19
+
15
20
  module PubSubModelSync
16
21
  class Error < StandardError; end
17
22
  # Your code goes here...
@@ -3,11 +3,26 @@
3
3
  module PubSubModelSync
4
4
  class Config
5
5
  cattr_accessor :listeners, default: []
6
- cattr_accessor :project, :credentials, :topic_name, :subscription_name
6
+ cattr_accessor :publishers, default: []
7
+ cattr_accessor :service_name, default: :google
7
8
  cattr_accessor :logger
9
+
10
+ # google service
11
+ cattr_accessor :project, :credentials, :topic_name, :subscription_name
12
+
13
+ # rabbitmq service
14
+ cattr_accessor :bunny_connection, :queue_name, :topic_name
15
+
16
+ # kafka service
17
+ cattr_accessor :kafka_connection, :topic_name
18
+
8
19
  def self.log(msg, kind = :info)
9
20
  msg = "PS_MSYNC ==> #{msg}"
10
- logger ? logger.send(kind, msg) : puts(msg)
21
+ if logger == :raise_error
22
+ kind == :error ? raise(msg) : puts(msg)
23
+ else
24
+ logger ? logger.send(kind, msg) : puts(msg)
25
+ end
11
26
  end
12
27
  end
13
28
  end
@@ -3,55 +3,24 @@
3
3
  require 'google/cloud/pubsub'
4
4
  module PubSubModelSync
5
5
  class Connector
6
- attr_accessor :service, :topic, :subscription, :config, :subscriber
6
+ attr_accessor :service
7
+ delegate :listen_messages, :publish, :stop, to: :service
7
8
 
8
9
  def initialize
9
- @config = PubSubModelSync::Config
10
- @service = Google::Cloud::Pubsub.new(project: config.project,
11
- credentials: config.credentials)
12
- @topic = service.topic(config.topic_name) ||
13
- service.create_topic(config.topic_name)
14
- end
15
-
16
- def listen_messages
17
- @subscription = subscribe_to_topic
18
- @subscriber = subscription.listen(&method(:process_message))
19
- log('Listener starting...')
20
- subscriber.start
21
- log('Listener started')
22
- sleep
23
- subscriber.stop.wait!
24
- log('Listener stopped')
25
- end
26
-
27
- def stop
28
- log('Listener stopping...')
29
- subscriber.stop!
10
+ @service = build_service
30
11
  end
31
12
 
32
13
  private
33
14
 
34
- def subscribe_to_topic
35
- topic.subscription(config.subscription_name) ||
36
- topic.subscribe(config.subscription_name)
37
- end
38
-
39
- def process_message(received_message)
40
- message = received_message.message
41
- attrs = message.attributes.symbolize_keys
42
- return unless attrs[:service_model_sync]
43
-
44
- data = JSON.parse(message.data).symbolize_keys
45
- args = [data, attrs[:klass], attrs[:action], attrs]
46
- PubSubModelSync::MessageProcessor.new(*args).process
47
- rescue => e
48
- log("Error processing message: #{[received_message, e.message]}")
49
- ensure
50
- received_message.acknowledge!
51
- end
52
-
53
- def log(msg)
54
- config.log(msg)
15
+ def build_service
16
+ case Config.service_name
17
+ when :google
18
+ PubSubModelSync::ServiceGoogle.new
19
+ when :kafka
20
+ PubSubModelSync::ServiceKafka.new
21
+ else # :rabbit_mq
22
+ PubSubModelSync::ServiceRabbit.new
23
+ end
55
24
  end
56
25
  end
57
26
  end
@@ -40,7 +40,6 @@ module PubSubModelSync
40
40
 
41
41
  # support for: create, update, destroy
42
42
  def call_listener(listener)
43
- listener_add_crud_settings(listener)
44
43
  model = find_model(listener)
45
44
  if attrs[:action].to_sym == :destroy
46
45
  model.destroy!
@@ -74,11 +73,6 @@ module PubSubModelSync
74
73
  end
75
74
  end
76
75
 
77
- def listener_add_crud_settings(listener)
78
- model_class = listener[:klass].constantize
79
- listener[:settings] = model_class.ps_msync_subscriber_settings
80
- end
81
-
82
76
  def log(message, kind = :info)
83
77
  PubSubModelSync::Config.log "#{message} ==> #{[data, attrs]}", kind
84
78
  end
@@ -7,6 +7,7 @@ module PubSubModelSync
7
7
  true
8
8
  end
9
9
  end
10
+
10
11
  class MockSubscriber
11
12
  def start
12
13
  true
@@ -15,12 +16,15 @@ module PubSubModelSync
15
16
  def stop
16
17
  @stop ||= MockStop.new
17
18
  end
19
+ alias stop! stop
18
20
  end
21
+
19
22
  class MockSubscription
20
23
  def listen(*_args)
21
24
  @listen ||= MockSubscriber.new
22
25
  end
23
26
  end
27
+
24
28
  class MockTopic
25
29
  def subscription(*_args)
26
30
  @subscription ||= MockSubscription.new
@@ -31,6 +35,7 @@ module PubSubModelSync
31
35
  true
32
36
  end
33
37
  end
38
+
34
39
  def topic(*_args)
35
40
  @topic ||= MockTopic.new
36
41
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class MockKafkaService
5
+ class MockProducer
6
+ def produce(*_args)
7
+ true
8
+ end
9
+
10
+ def deliver_messages(*_args)
11
+ true
12
+ end
13
+
14
+ def shutdown
15
+ true
16
+ end
17
+ end
18
+
19
+ class MockConsumer
20
+ def each_message(*_args)
21
+ true
22
+ end
23
+
24
+ def stop(*_args)
25
+ true
26
+ end
27
+
28
+ def subscribe(*_args)
29
+ true
30
+ end
31
+ end
32
+
33
+ def producer(*_args)
34
+ MockProducer.new
35
+ end
36
+
37
+ def consumer(*_args)
38
+ MockConsumer.new
39
+ end
40
+
41
+ def close
42
+ true
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class MockRabbitService
5
+ class MockTopic
6
+ def publish(*_args)
7
+ true
8
+ end
9
+ end
10
+
11
+ class MockQueue
12
+ def bind(*_args)
13
+ true
14
+ end
15
+
16
+ def subscribe(*_args)
17
+ true
18
+ end
19
+
20
+ def name
21
+ 'name'
22
+ end
23
+ end
24
+
25
+ class MockChannel
26
+ def queue(*_args)
27
+ @queue ||= MockQueue.new
28
+ end
29
+
30
+ def topic(*_args)
31
+ @topic ||= MockTopic.new
32
+ end
33
+ end
34
+
35
+ def create_channel(*_args)
36
+ @create_channel ||= MockChannel.new
37
+ end
38
+ alias channel create_channel
39
+
40
+ def start
41
+ true
42
+ end
43
+
44
+ def close
45
+ true
46
+ end
47
+ end
48
+ end
@@ -9,34 +9,37 @@ module PubSubModelSync
9
9
 
10
10
  def publish_data(klass, data, action)
11
11
  attributes = self.class.build_attrs(klass, action)
12
- publish(data, attributes)
12
+ connector.publish(data, attributes)
13
13
  end
14
14
 
15
15
  # @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
16
16
  def publish_model(model, action, settings = nil)
17
- settings ||= model.class.ps_msync_publisher_settings
17
+ settings ||= model.class.ps_publisher_info(action)
18
18
  attributes = build_model_attrs(model, action, settings)
19
19
  data = {}
20
- if action != 'destroy'
21
- data = model.as_json(only: settings[:attrs], methods: settings[:attrs])
22
- end
23
- publish(data.symbolize_keys, attributes)
20
+ data = build_model_data(model, settings[:attrs]) if action != :destroy
21
+ connector.publish(data.symbolize_keys, attributes)
24
22
  end
25
23
 
26
24
  def self.build_attrs(klass, action, id = nil)
27
25
  {
28
26
  klass: klass.to_s,
29
27
  action: action.to_sym,
30
- id: id,
31
- service_model_sync: true
28
+ id: id
32
29
  }
33
30
  end
34
31
 
35
32
  private
36
33
 
37
- def publish(data, attributes)
38
- log("Publishing: #{[data, attributes]}")
39
- connector.topic.publish(data.to_json, attributes)
34
+ def build_model_data(model, model_props)
35
+ source_props = model_props.map { |prop| prop.to_s.split(':').first }
36
+ data = model.as_json(only: source_props, methods: source_props)
37
+ aliased_props = model_props.select { |prop| prop.to_s.include?(':') }
38
+ aliased_props.each do |prop|
39
+ source, target = prop.to_s.split(':')
40
+ data[target] = data.delete(source)
41
+ end
42
+ data.symbolize_keys
40
43
  end
41
44
 
42
45
  def build_model_attrs(model, action, settings)
@@ -7,41 +7,44 @@ module PubSubModelSync
7
7
  end
8
8
 
9
9
  # Permit to skip a publish callback
10
- def ps_msync_skip_for?(_action)
10
+ def ps_skip_for?(_action)
11
11
  false
12
12
  end
13
13
 
14
14
  module ClassMethods
15
15
  # Permit to publish crud actions (:create, :update, :destroy)
16
16
  # @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
17
- def ps_msync_publish(attrs, settings = {})
17
+ def ps_publish(attrs, settings = {})
18
18
  actions = settings.delete(:actions) || %i[create update destroy]
19
- @ps_msync_publisher_settings = settings.merge(attrs: attrs)
20
- ps_msync_register_callbacks(actions)
19
+ actions.each do |action|
20
+ info = settings.merge(klass: name, action: action, attrs: attrs)
21
+ PubSubModelSync::Config.publishers << info
22
+ ps_register_callback(action.to_sym, info)
23
+ end
21
24
  end
22
25
 
23
- def ps_msync_publisher_settings
24
- @ps_msync_publisher_settings
26
+ def ps_publisher_info(action = :create)
27
+ PubSubModelSync::Config.publishers.select do |listener|
28
+ listener[:klass] == name && listener[:action] == action
29
+ end.last
25
30
  end
26
31
 
27
- def ps_msync_class_publish(data, action:, as_klass: nil)
32
+ def ps_class_publish(data, action:, as_klass: nil)
28
33
  as_klass = (as_klass || name).to_s
29
- ps_msync_publisher.publish_data(as_klass, data, action.to_sym)
34
+ ps_publisher_service.publish_data(as_klass, data, action.to_sym)
30
35
  end
31
36
 
32
- def ps_msync_publisher
37
+ def ps_publisher_service
33
38
  PubSubModelSync::Publisher.new
34
39
  end
35
40
 
36
41
  private
37
42
 
38
- def ps_msync_register_callbacks(actions)
39
- actions.each do |action|
40
- after_commit(on: action) do |model|
41
- unless model.ps_msync_skip_for?(action)
42
- publisher = model.class.ps_msync_publisher
43
- publisher.publish_model(model, action.to_sym)
44
- end
43
+ def ps_register_callback(action, info)
44
+ after_commit(on: action) do |model|
45
+ unless model.ps_skip_for?(action)
46
+ service = model.class.ps_publisher_service
47
+ service.publish_model(model, action.to_sym, info)
45
48
  end
46
49
  end
47
50
  end
@@ -6,6 +6,10 @@ module PubSubModelSync
6
6
  class ShutDown < StandardError; end
7
7
  attr_accessor :connector
8
8
 
9
+ def initialize
10
+ @connector = PubSubModelSync::Connector.new
11
+ end
12
+
9
13
  def run
10
14
  trap_signals!
11
15
  preload_framework!
@@ -17,7 +21,6 @@ module PubSubModelSync
17
21
  private
18
22
 
19
23
  def start_listeners
20
- @connector = PubSubModelSync::Connector.new
21
24
  connector.listen_messages
22
25
  end
23
26
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class ServiceBase
5
+ SERVICE_KEY = 'service_model_sync'
6
+
7
+ def listen_messages
8
+ raise 'method :listen_messages must be defined in service'
9
+ end
10
+
11
+ def publish(_data, _attributes)
12
+ raise 'method :publish must be defined in service'
13
+ end
14
+
15
+ def stop
16
+ raise 'method :stop must be defined in service'
17
+ end
18
+
19
+ private
20
+
21
+ # @param payload (String JSON): '{"data":{},"attributes":{..}}'
22
+ # refer: PubSubModelSync::Publisher (.publish_model | .publish_data)
23
+ def perform_message(payload)
24
+ data, attrs = parse_message_payload(payload)
25
+ args = [data, attrs[:klass], attrs[:action], attrs]
26
+ PubSubModelSync::MessageProcessor.new(*args).process
27
+ end
28
+
29
+ def parse_message_payload(payload)
30
+ message_payload = JSON.parse(payload).symbolize_keys
31
+ data = message_payload[:data].symbolize_keys
32
+ attrs = message_payload[:attributes].symbolize_keys
33
+ [data, attrs]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'google/cloud/pubsub'
5
+ rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ end
7
+
8
+ module PubSubModelSync
9
+ class ServiceGoogle < ServiceBase
10
+ attr_accessor :service, :topic, :subscription, :config, :subscriber
11
+
12
+ def initialize
13
+ @config = PubSubModelSync::Config
14
+ @service = Google::Cloud::Pubsub.new(project: config.project,
15
+ credentials: config.credentials)
16
+ @topic = service.topic(config.topic_name) ||
17
+ service.create_topic(config.topic_name)
18
+ end
19
+
20
+ def listen_messages
21
+ @subscription = subscribe_to_topic
22
+ @subscriber = subscription.listen(&method(:process_message))
23
+ log('Listener starting...')
24
+ subscriber.start
25
+ log('Listener started')
26
+ sleep
27
+ subscriber.stop.wait!
28
+ log('Listener stopped')
29
+ end
30
+
31
+ def publish(data, attributes)
32
+ log("Publishing message: #{[data, attributes]}")
33
+
34
+ payload = { data: data, attributes: attributes }.to_json
35
+ topic.publish(payload, { SERVICE_KEY => true })
36
+ end
37
+
38
+ def stop
39
+ log('Listener stopping...')
40
+ subscriber.stop!
41
+ end
42
+
43
+ private
44
+
45
+ def subscribe_to_topic
46
+ topic.subscription(config.subscription_name) ||
47
+ topic.subscribe(config.subscription_name)
48
+ end
49
+
50
+ def process_message(received_message)
51
+ message = received_message.message
52
+ return unless message.attributes[SERVICE_KEY]
53
+
54
+ perform_message(message.data)
55
+ rescue => e
56
+ log("Error processing message: #{[received_message, e.message]}", :error)
57
+ ensure
58
+ received_message.acknowledge!
59
+ end
60
+
61
+ def log(msg, kind = :info)
62
+ config.log("Google Service ==> #{msg}", kind)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'kafka'
5
+ rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ end
7
+
8
+ module PubSubModelSync
9
+ class ServiceKafka < ServiceBase
10
+ cattr_accessor :producer
11
+
12
+ attr_accessor :service, :consumer
13
+ attr_accessor :config
14
+ CONSUMER_GROUP = 'service_model_sync'
15
+
16
+ def initialize
17
+ @config = PubSubModelSync::Config
18
+ @service = Kafka.new(*config.kafka_connection)
19
+ end
20
+
21
+ def listen_messages
22
+ log('Listener starting...')
23
+ start_consumer
24
+ consumer.each_message(&method(:process_message))
25
+ rescue PubSubModelSync::Runner::ShutDown
26
+ raise
27
+ rescue => e
28
+ log("Error listening message: #{[e.message, e.backtrace]}", :error)
29
+ end
30
+
31
+ def publish(data, attributes)
32
+ log("Publishing: #{[data, attributes]}")
33
+ payload = { data: data, attributes: attributes }
34
+ producer.produce(payload.to_json, message_settings)
35
+ producer.deliver_messages
36
+ rescue => e
37
+ info = [data, attributes, e.message, e.backtrace]
38
+ log("Error publishing: #{info}", :error)
39
+ end
40
+
41
+ def stop
42
+ log('Listener stopping...')
43
+ consumer.stop
44
+ end
45
+
46
+ private
47
+
48
+ def message_settings
49
+ { topic: config.topic_name, headers: { SERVICE_KEY => true } }
50
+ end
51
+
52
+ def start_consumer
53
+ @consumer = service.consumer(group_id: CONSUMER_GROUP)
54
+ consumer.subscribe(config.topic_name)
55
+ end
56
+
57
+ def producer
58
+ return self.class.producer if self.class.producer
59
+
60
+ at_exit { self.class.producer.shutdown }
61
+ self.class.producer = service.producer
62
+ end
63
+
64
+ def process_message(message)
65
+ return unless message.headers[SERVICE_KEY]
66
+
67
+ perform_message(message.value)
68
+ rescue => e
69
+ error = [message, e.message, e.backtrace]
70
+ log("Error processing message: #{error}", :error)
71
+ end
72
+
73
+ def log(msg, kind = :info)
74
+ config.log("Kafka Service ==> #{msg}", kind)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bunny'
5
+ rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ end
7
+
8
+ module PubSubModelSync
9
+ class ServiceRabbit < ServiceBase
10
+ attr_accessor :service, :channel, :queue, :topic
11
+ attr_accessor :config
12
+
13
+ def initialize
14
+ @config = PubSubModelSync::Config
15
+ @service = Bunny.new(*config.bunny_connection)
16
+ end
17
+
18
+ def listen_messages
19
+ log('Listener starting...')
20
+ subscribe_to_queue
21
+ log('Listener started')
22
+ queue.subscribe(block: true, manual_ack: false, &method(:process_message))
23
+ rescue PubSubModelSync::Runner::ShutDown
24
+ raise
25
+ rescue => e
26
+ log("Error listening message: #{[e.message, e.backtrace]}", :error)
27
+ end
28
+
29
+ def publish(data, attributes)
30
+ log("Publishing: #{[data, attributes]}")
31
+ subscribe_to_queue
32
+ payload = { data: data, attributes: attributes }
33
+ topic.publish(payload.to_json, message_settings)
34
+ rescue => e
35
+ info = [data, attributes, e.message, e.backtrace]
36
+ log("Error publishing: #{info}", :error)
37
+ end
38
+
39
+ def stop
40
+ log('Listener stopping...')
41
+ service.close
42
+ end
43
+
44
+ private
45
+
46
+ def message_settings
47
+ { routing_key: queue.name, type: SERVICE_KEY }
48
+ end
49
+
50
+ def process_message(_delivery_info, meta_info, payload)
51
+ return unless meta_info[:type] == SERVICE_KEY
52
+
53
+ perform_message(payload)
54
+ rescue => e
55
+ error = [payload, e.message, e.backtrace]
56
+ log("Error processing message: #{error}", :error)
57
+ end
58
+
59
+ def subscribe_to_queue
60
+ service.start
61
+ @channel = service.create_channel
62
+ queue_settings = { durable: true, auto_delete: false }
63
+ @queue = channel.queue(config.queue_name, queue_settings)
64
+ subscribe_to_topic
65
+ end
66
+
67
+ def subscribe_to_topic
68
+ @topic = channel.topic(config.topic_name)
69
+ queue.bind(topic, routing_key: queue.name)
70
+ end
71
+
72
+ def log(msg, kind = :info)
73
+ config.log("Rabbit Service ==> #{msg}", kind)
74
+ end
75
+ end
76
+ end
@@ -8,32 +8,35 @@ module PubSubModelSync
8
8
 
9
9
  module ClassMethods
10
10
  # @param settings (Hash): { as_klass: nil, actions: nil, id: nil }
11
- def ps_msync_subscribe(attrs, settings = {})
12
- settings[:as_klass] = (settings[:as_klass] || name).to_s
11
+ def ps_subscribe(attrs, settings = {})
12
+ as_klass = (settings[:as_klass] || name).to_s
13
13
  actions = settings.delete(:actions) || %i[create update destroy]
14
- @ps_msync_subscriber_settings = { attrs: attrs }.merge(settings)
14
+ settings = settings.merge(attrs: attrs)
15
15
  actions.each do |action|
16
- add_ps_msync_subscriber(settings[:as_klass], action, action, false)
16
+ add_ps_subscriber(as_klass, action, action, false, settings)
17
17
  end
18
18
  end
19
19
 
20
- def ps_msync_class_subscribe(action, as_action: nil, as_klass: nil)
21
- add_ps_msync_subscriber(as_klass, action, as_action, true)
20
+ def ps_class_subscribe(action, as_action: nil, as_klass: nil)
21
+ add_ps_subscriber(as_klass, action, as_action, true, {})
22
22
  end
23
23
 
24
- def ps_msync_subscriber_settings
25
- @ps_msync_subscriber_settings || {}
24
+ def ps_subscriber(action = :create)
25
+ PubSubModelSync::Config.listeners.select do |listener|
26
+ listener[:klass] == name && listener[:action] == action
27
+ end.last
26
28
  end
27
29
 
28
30
  private
29
31
 
30
- def add_ps_msync_subscriber(as_klass, action, as_action, direct_mode)
32
+ def add_ps_subscriber(as_klass, action, as_action, direct_mode, settings)
31
33
  listener = {
32
34
  klass: name,
33
35
  as_klass: (as_klass || name).to_s,
34
36
  action: action.to_sym,
35
37
  as_action: (as_action || action).to_sym,
36
- direct_mode: direct_mode
38
+ direct_mode: direct_mode,
39
+ settings: settings
37
40
  }
38
41
  PubSubModelSync::Config.listeners << listener
39
42
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.5'
5
5
  end
@@ -33,7 +33,6 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ['lib']
34
34
 
35
35
  spec.add_dependency 'activesupport'
36
- spec.add_dependency 'google-cloud-pubsub'
37
36
  spec.add_dependency 'rails'
38
37
 
39
38
  spec.add_development_dependency 'bundler'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pub_sub_model_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-12 00:00:00.000000000 Z
11
+ date: 2020-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: google-cloud-pubsub
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rails
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -119,6 +105,7 @@ files:
119
105
  - ".rspec"
120
106
  - ".rubocop.yml"
121
107
  - ".travis.yml"
108
+ - CHANGELOG.md
122
109
  - CODE_OF_CONDUCT.md
123
110
  - Gemfile
124
111
  - Gemfile.lock
@@ -132,10 +119,16 @@ files:
132
119
  - lib/pub_sub_model_sync/connector.rb
133
120
  - lib/pub_sub_model_sync/message_processor.rb
134
121
  - lib/pub_sub_model_sync/mock_google_service.rb
122
+ - lib/pub_sub_model_sync/mock_kafka_service.rb
123
+ - lib/pub_sub_model_sync/mock_rabbit_service.rb
135
124
  - lib/pub_sub_model_sync/publisher.rb
136
125
  - lib/pub_sub_model_sync/publisher_concern.rb
137
126
  - lib/pub_sub_model_sync/railtie.rb
138
127
  - lib/pub_sub_model_sync/runner.rb
128
+ - lib/pub_sub_model_sync/service_base.rb
129
+ - lib/pub_sub_model_sync/service_google.rb
130
+ - lib/pub_sub_model_sync/service_kafka.rb
131
+ - lib/pub_sub_model_sync/service_rabbit.rb
139
132
  - lib/pub_sub_model_sync/subscriber_concern.rb
140
133
  - lib/pub_sub_model_sync/tasks/worker.rake
141
134
  - lib/pub_sub_model_sync/version.rb