pub_sub_model_sync 0.5.1.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +1 -1
- data/README.md +31 -18
- data/lib/pub_sub_model_sync/base.rb +7 -0
- data/lib/pub_sub_model_sync/config.rb +4 -4
- data/lib/pub_sub_model_sync/message_processor.rb +10 -8
- data/lib/pub_sub_model_sync/message_publisher.rb +4 -5
- data/lib/pub_sub_model_sync/payload.rb +12 -0
- data/lib/pub_sub_model_sync/publisher.rb +1 -1
- data/lib/pub_sub_model_sync/service_base.rb +2 -0
- data/lib/pub_sub_model_sync/service_google.rb +6 -4
- data/lib/pub_sub_model_sync/service_kafka.rb +10 -10
- data/lib/pub_sub_model_sync/service_rabbit.rb +10 -5
- data/lib/pub_sub_model_sync/subscriber.rb +3 -3
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a19e13bd4fa78cde78749ffa47096832c31abbc3fdc10e3938988c2c4a73d44
|
4
|
+
data.tar.gz: a6abb31c4d900dbb1f7307833e88056ac6d1621bbed46b4136f289602b5aab5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 128c4edf32925745e271933c0416c44740c1b553c61f04efb75111787404a419b70185a55527db87d28f4c0d03b534d01ef8e01fa46191b539c1766c315de685
|
7
|
+
data.tar.gz: 4e73d7f16aead95368d4e6830530259e1d664fb0a23b4fc0f5f49e13aead8b5fbc1778aac8c2ea5a0e7c4f996a10f925d3af51c0730bf962ac6d712a11f7bb84
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
# 0.5.5 (January 11, 2021)
|
4
|
+
- feat: google-pub/sub receive messages in the same order they were delivered
|
5
|
+
|
6
|
+
# 0.5.4.1 (January 8, 2021)
|
7
|
+
- fix: google-pub/sub receive messages sequentially and not in parallel (default 5 threads).
|
8
|
+
|
9
|
+
# 0.5.4 (January 8, 2021)
|
10
|
+
- fix: exclude identifiers when syncing model
|
11
|
+
- feat: callbacks support for future extra params
|
12
|
+
- feat: make connectors configurable
|
13
|
+
- feat: add :process! and :process, :publish!, :publish methods to payload
|
14
|
+
- feat: auto retry 2 times when "could not obtain a database connection within 5.000 seconds..." error occurs
|
15
|
+
|
16
|
+
# 0.5.3 (December 30, 2020)
|
17
|
+
- fix: kafka consume all messages from different apps
|
18
|
+
- style: use the correct consumer key
|
19
|
+
|
20
|
+
# 0.5.2 (December 30, 2020)
|
21
|
+
- fix: rabbitmq deliver messages to all subscribers
|
22
|
+
- fix: rabbitmq persist messages to recover after restarting
|
23
|
+
|
3
24
|
# 0.5.1.1 (December 29, 2020)
|
4
25
|
- Hotfix: auto convert class name into string
|
5
26
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -28,10 +28,9 @@ And then execute: $ bundle install
|
|
28
28
|
```ruby
|
29
29
|
# initializers/pub_sub_config.rb
|
30
30
|
PubSubModelSync::Config.service_name = :google
|
31
|
-
PubSubModelSync::Config.project = 'project-id'
|
31
|
+
PubSubModelSync::Config.project = 'google-project-id'
|
32
32
|
PubSubModelSync::Config.credentials = 'path-to-the-config'
|
33
33
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
34
|
-
PubSubModelSync::Config.subscription_name = 'p1-subscriber'
|
35
34
|
```
|
36
35
|
See details here:
|
37
36
|
https://github.com/googleapis/google-cloud-ruby/tree/master/google-cloud-pubsub
|
@@ -40,7 +39,7 @@ And then execute: $ bundle install
|
|
40
39
|
```ruby
|
41
40
|
PubSubModelSync::Config.service_name = :rabbitmq
|
42
41
|
PubSubModelSync::Config.bunny_connection = 'amqp://guest:guest@localhost'
|
43
|
-
PubSubModelSync::Config.queue_name = ''
|
42
|
+
PubSubModelSync::Config.queue_name = 'model-sync'
|
44
43
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
45
44
|
```
|
46
45
|
See details here: https://github.com/ruby-amqp/bunny
|
@@ -48,7 +47,7 @@ And then execute: $ bundle install
|
|
48
47
|
- configuration for Apache Kafka (You need kafka installed)
|
49
48
|
```ruby
|
50
49
|
PubSubModelSync::Config.service_name = :kafka
|
51
|
-
PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "localhost:2121"], logger: Rails.logger]
|
50
|
+
PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "localhost:2121"], { logger: Rails.logger }]
|
52
51
|
PubSubModelSync::Config.topic_name = 'sample-topic'
|
53
52
|
```
|
54
53
|
See details here: https://github.com/zendesk/ruby-kafka
|
@@ -207,10 +206,13 @@ Note: Be careful with collision of names
|
|
207
206
|
* action_name: (required, :sim) Action name
|
208
207
|
* as_klass: (optional, :string) Custom class name (Default current model name)
|
209
208
|
|
210
|
-
-
|
209
|
+
- Payload actions
|
211
210
|
```ruby
|
212
211
|
payload = PubSubModelSync::Payload.new({ title: 'hello' }, { action: :greeting, klass: 'User' })
|
213
|
-
payload.publish!
|
212
|
+
payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback
|
213
|
+
payload.publish # publishes notification data. On error does not raise exception but calls ```:on_error_publishing``` callback
|
214
|
+
payload.process! # process a notification data. It raises exception if fails and does not call ```.on_error_processing``` callback
|
215
|
+
payload.publish # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback
|
214
216
|
```
|
215
217
|
|
216
218
|
- Get crud publisher configured for the class
|
@@ -283,30 +285,41 @@ config = PubSubModelSync::Config
|
|
283
285
|
config.debug = true
|
284
286
|
```
|
285
287
|
|
286
|
-
-
|
288
|
+
- ```.subscription_name = 'app-2'```
|
289
|
+
Permit to define a custom consumer identifier (Default: Rails application name)
|
290
|
+
- ```.debug = true```
|
287
291
|
(true/false*) => show advanced log messages
|
288
|
-
-
|
292
|
+
- ```.logger = Rails.logger```
|
289
293
|
(Logger) => define custom logger
|
290
|
-
-
|
294
|
+
- ```.disabled_callback_publisher = ->(_model, _action) { false }```
|
291
295
|
(true/false*) => if true, does not listen model callbacks for auto sync (Create/Update/Destroy)
|
292
|
-
-
|
296
|
+
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
293
297
|
(Proc) => called before processing received message (:cancel can be returned to skip processing)
|
294
|
-
-
|
298
|
+
- ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
|
295
299
|
(Proc) => called when a message was successfully processed
|
296
|
-
-
|
297
|
-
(Proc) => called when a message failed when processing
|
298
|
-
-
|
300
|
+
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
301
|
+
(Proc) => called when a message failed when processing (delayed_job or similar can be used for retrying)
|
302
|
+
- ```.on_before_publish = ->(payload) { puts payload }```
|
299
303
|
(Proc) => called before publishing a message (:cancel can be returned to skip publishing)
|
300
|
-
-
|
304
|
+
- ```.on_after_publish = ->(payload) { puts payload }```
|
301
305
|
(Proc) => called after publishing a message
|
302
|
-
-
|
303
|
-
(Proc) => called when failed publishing a message
|
306
|
+
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
307
|
+
(Proc) => called when failed publishing a message (delayed_job or similar can be used for retrying)
|
304
308
|
|
305
309
|
## TODO
|
306
310
|
- Add alias attributes when subscribing (similar to publisher)
|
307
311
|
- Add flag ```model.ps_processing``` to indicate that the current transaction is being processed by pub/sub
|
308
312
|
- Auto publish update only if payload has changed
|
309
|
-
- On delete, payload must only be composed by ids
|
313
|
+
- On delete, payload must only be composed by ids
|
314
|
+
- Change notifications into messages
|
315
|
+
|
316
|
+
## Q&A
|
317
|
+
- Error "could not obtain a connection from the pool within 5.000 seconds"
|
318
|
+
This problem occurs because pub/sub dependencies (kafka, google-pubsub, rabbitmq) use many threads to perform notifications where the qty of threads is greater than qty of DB pools ([Google pubsub info](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-pubsub/lib/google/cloud/pubsub/subscription.rb#L888))
|
319
|
+
To fix the problem, edit config/database.yml and increase the quantity of ```pool: 10```
|
320
|
+
- Google pubsub: How to process notifications parallely and not sequentially (default 1 thread)?
|
321
|
+
```ruby PubSubModelSync::ServiceGoogle::LISTEN_SETTINGS = { threads: { callback: qty_threads } } ```
|
322
|
+
Note: by this way some notifications can be processed before others thus missing relationship errors can appear
|
310
323
|
|
311
324
|
## Contributing
|
312
325
|
|
@@ -10,12 +10,12 @@ module PubSubModelSync
|
|
10
10
|
cattr_accessor(:debug) { false }
|
11
11
|
cattr_accessor :logger # LoggerInst
|
12
12
|
|
13
|
-
cattr_accessor(:on_before_processing) { ->(_payload,
|
14
|
-
cattr_accessor(:on_success_processing) { ->(_payload,
|
15
|
-
cattr_accessor(:on_error_processing) { ->(_exception,
|
13
|
+
cattr_accessor(:on_before_processing) { ->(_payload, _info) {} } # return :cancel to skip
|
14
|
+
cattr_accessor(:on_success_processing) { ->(_payload, _info) {} }
|
15
|
+
cattr_accessor(:on_error_processing) { ->(_exception, _info) {} }
|
16
16
|
cattr_accessor(:on_before_publish) { ->(_payload) {} } # return :cancel to skip
|
17
17
|
cattr_accessor(:on_after_publish) { ->(_payload) {} }
|
18
|
-
cattr_accessor(:on_error_publish) { ->(_exception,
|
18
|
+
cattr_accessor(:on_error_publish) { ->(_exception, _info) {} }
|
19
19
|
cattr_accessor(:disabled_callback_publisher) { ->(_model, _action) { false } }
|
20
20
|
|
21
21
|
# google service
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class MessageProcessor < PubSubModelSync::Base
|
5
|
-
attr_accessor :payload
|
5
|
+
attr_accessor :payload, :raise_error
|
6
6
|
|
7
7
|
# @param payload (Payload): payload to be delivered
|
8
8
|
# @Deprecated: def initialize(data, klass, action)
|
@@ -24,23 +24,25 @@ module PubSubModelSync
|
|
24
24
|
def run_subscriber(subscriber)
|
25
25
|
return unless processable?(subscriber)
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
retry_error(ActiveRecord::ConnectionTimeoutError, qty: 2) do
|
28
|
+
subscriber.process!(payload)
|
29
|
+
res = config.on_success_processing.call(payload, { subscriber: subscriber })
|
30
|
+
log "processed message with: #{payload.inspect}" if res != :skip_log
|
31
|
+
end
|
30
32
|
rescue => e
|
31
|
-
print_subscriber_error(e)
|
33
|
+
raise_error ? raise : print_subscriber_error(e, subscriber)
|
32
34
|
end
|
33
35
|
|
34
36
|
def processable?(subscriber)
|
35
|
-
cancel = config.on_before_processing.call(payload, subscriber) == :cancel
|
37
|
+
cancel = config.on_before_processing.call(payload, { subscriber: subscriber }) == :cancel
|
36
38
|
log("process message cancelled: #{payload}") if cancel && config.debug
|
37
39
|
!cancel
|
38
40
|
end
|
39
41
|
|
40
42
|
# @param error (Error)
|
41
|
-
def print_subscriber_error(error)
|
43
|
+
def print_subscriber_error(error, subscriber)
|
42
44
|
info = [payload, error.message, error.backtrace]
|
43
|
-
res = config.on_error_processing.call(error, payload)
|
45
|
+
res = config.on_error_processing.call(error, { payload: payload, subscriber: subscriber })
|
44
46
|
log("Error processing message: #{info}", :error) if res != :skip_log
|
45
47
|
end
|
46
48
|
|
@@ -19,8 +19,7 @@ module PubSubModelSync
|
|
19
19
|
return if model.ps_skip_sync?(action)
|
20
20
|
|
21
21
|
publisher ||= model.class.ps_publisher(action)
|
22
|
-
|
23
|
-
payload = PubSubModelSync::Payload.new(payload_info[:data], payload_info[:attrs])
|
22
|
+
payload = publisher.payload(model, action)
|
24
23
|
res_before = model.ps_before_sync(action, payload.data)
|
25
24
|
return if res_before == :cancel
|
26
25
|
|
@@ -28,7 +27,7 @@ module PubSubModelSync
|
|
28
27
|
model.ps_after_sync(action, payload.data)
|
29
28
|
end
|
30
29
|
|
31
|
-
def publish(payload)
|
30
|
+
def publish(payload, raise_error: false)
|
32
31
|
if config.on_before_publish.call(payload) == :cancel
|
33
32
|
log("Publish message cancelled: #{payload}") if config.debug
|
34
33
|
return
|
@@ -38,14 +37,14 @@ module PubSubModelSync
|
|
38
37
|
connector.publish(payload)
|
39
38
|
config.on_after_publish.call(payload)
|
40
39
|
rescue => e
|
41
|
-
notify_error(e, payload)
|
40
|
+
raise_error ? raise : notify_error(e, payload)
|
42
41
|
end
|
43
42
|
|
44
43
|
private
|
45
44
|
|
46
45
|
def notify_error(exception, payload)
|
47
46
|
info = [payload, exception.message, exception.backtrace]
|
48
|
-
res = config.on_error_publish.call(exception, payload)
|
47
|
+
res = config.on_error_publish.call(exception, { payload: payload })
|
49
48
|
log("Error publishing: #{info}", :error) if res != :skip_log
|
50
49
|
end
|
51
50
|
end
|
@@ -26,11 +26,23 @@ module PubSubModelSync
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def process!
|
29
|
+
process do |publisher|
|
30
|
+
publisher.raise_error = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def process
|
29
35
|
publisher = PubSubModelSync::MessageProcessor.new(self)
|
36
|
+
yield(publisher) if block_given?
|
30
37
|
publisher.process
|
31
38
|
end
|
32
39
|
|
33
40
|
def publish!
|
41
|
+
klass = PubSubModelSync::MessagePublisher
|
42
|
+
klass.publish(self, raise_error: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def publish
|
34
46
|
klass = PubSubModelSync::MessagePublisher
|
35
47
|
klass.publish(self)
|
36
48
|
end
|
@@ -4,6 +4,8 @@ require 'pub_sub_model_sync/payload'
|
|
4
4
|
module PubSubModelSync
|
5
5
|
class ServiceBase < PubSubModelSync::Base
|
6
6
|
SERVICE_KEY = 'service_model_sync'
|
7
|
+
PUBLISH_SETTINGS = {}.freeze
|
8
|
+
LISTEN_SETTINGS = {}.freeze
|
7
9
|
|
8
10
|
def listen_messages
|
9
11
|
raise 'method :listen_messages must be defined in service'
|
@@ -7,6 +7,8 @@ end
|
|
7
7
|
|
8
8
|
module PubSubModelSync
|
9
9
|
class ServiceGoogle < ServiceBase
|
10
|
+
LISTEN_SETTINGS = { threads: { callback: 1 } }.freeze
|
11
|
+
SUBSCRIPTION_SETTINGS = { message_ordering: true }.freeze
|
10
12
|
attr_accessor :service, :topic, :subscription, :subscriber
|
11
13
|
|
12
14
|
def initialize
|
@@ -18,7 +20,7 @@ module PubSubModelSync
|
|
18
20
|
|
19
21
|
def listen_messages
|
20
22
|
@subscription = subscribe_to_topic
|
21
|
-
@subscriber = subscription.listen(&method(:process_message))
|
23
|
+
@subscriber = subscription.listen(LISTEN_SETTINGS, &method(:process_message))
|
22
24
|
log('Listener starting...')
|
23
25
|
subscriber.start
|
24
26
|
log('Listener started')
|
@@ -28,7 +30,7 @@ module PubSubModelSync
|
|
28
30
|
end
|
29
31
|
|
30
32
|
def publish(payload)
|
31
|
-
topic.publish(payload.to_json, { SERVICE_KEY => true })
|
33
|
+
topic.publish(payload.to_json, { SERVICE_KEY => true }.merge(PUBLISH_SETTINGS))
|
32
34
|
end
|
33
35
|
|
34
36
|
def stop
|
@@ -39,8 +41,8 @@ module PubSubModelSync
|
|
39
41
|
private
|
40
42
|
|
41
43
|
def subscribe_to_topic
|
42
|
-
topic.subscription(config.
|
43
|
-
topic.subscribe(config.
|
44
|
+
topic.subscription(config.subscription_key) ||
|
45
|
+
topic.subscribe(config.subscription_key, SUBSCRIPTION_SETTINGS)
|
44
46
|
end
|
45
47
|
|
46
48
|
def process_message(received_message)
|
@@ -10,17 +10,17 @@ module PubSubModelSync
|
|
10
10
|
cattr_accessor :producer
|
11
11
|
attr_accessor :config, :service, :consumer
|
12
12
|
|
13
|
-
CONSUMER_GROUP = 'service_model_sync'
|
14
|
-
|
15
13
|
def initialize
|
16
14
|
@config = PubSubModelSync::Config
|
17
|
-
|
15
|
+
settings = config.kafka_connection
|
16
|
+
settings[1][:client_id] ||= config.subscription_key
|
17
|
+
@service = Kafka.new(*settings)
|
18
18
|
end
|
19
19
|
|
20
20
|
def listen_messages
|
21
21
|
log('Listener starting...')
|
22
22
|
start_consumer
|
23
|
-
consumer.each_message(&method(:process_message))
|
23
|
+
consumer.each_message(LISTEN_SETTINGS, &method(:process_message))
|
24
24
|
rescue PubSubModelSync::Runner::ShutDown
|
25
25
|
log('Listener stopped')
|
26
26
|
rescue => e
|
@@ -28,7 +28,11 @@ module PubSubModelSync
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def publish(payload)
|
31
|
-
|
31
|
+
settings = {
|
32
|
+
topic: config.topic_name,
|
33
|
+
headers: { SERVICE_KEY => true }
|
34
|
+
}.merge(PUBLISH_SETTINGS)
|
35
|
+
producer.produce(payload.to_json, settings)
|
32
36
|
producer.deliver_messages
|
33
37
|
end
|
34
38
|
|
@@ -39,12 +43,8 @@ module PubSubModelSync
|
|
39
43
|
|
40
44
|
private
|
41
45
|
|
42
|
-
def message_settings
|
43
|
-
{ topic: config.topic_name, headers: { SERVICE_KEY => true } }
|
44
|
-
end
|
45
|
-
|
46
46
|
def start_consumer
|
47
|
-
@consumer = service.consumer(group_id:
|
47
|
+
@consumer = service.consumer(group_id: config.subscription_key)
|
48
48
|
consumer.subscribe(config.topic_name)
|
49
49
|
end
|
50
50
|
|
@@ -40,6 +40,7 @@ module PubSubModelSync
|
|
40
40
|
|
41
41
|
def stop
|
42
42
|
log('Listener stopping...')
|
43
|
+
channel&.close
|
43
44
|
service.close
|
44
45
|
end
|
45
46
|
|
@@ -48,12 +49,17 @@ module PubSubModelSync
|
|
48
49
|
def message_settings
|
49
50
|
{
|
50
51
|
routing_key: queue.name,
|
51
|
-
type: SERVICE_KEY
|
52
|
-
|
52
|
+
type: SERVICE_KEY,
|
53
|
+
persistent: true
|
54
|
+
}.merge(PUBLISH_SETTINGS)
|
55
|
+
end
|
56
|
+
|
57
|
+
def queue_settings
|
58
|
+
{ durable: true, auto_delete: false }
|
53
59
|
end
|
54
60
|
|
55
61
|
def subscribe_settings
|
56
|
-
{ manual_ack: false }
|
62
|
+
{ manual_ack: false }.merge(LISTEN_SETTINGS)
|
57
63
|
end
|
58
64
|
|
59
65
|
def process_message(_delivery_info, meta_info, payload)
|
@@ -65,8 +71,7 @@ module PubSubModelSync
|
|
65
71
|
def subscribe_to_queue
|
66
72
|
service.start
|
67
73
|
@channel = service.create_channel
|
68
|
-
|
69
|
-
@queue = channel.queue(config.queue_name, queue_settings)
|
74
|
+
@queue = channel.queue(config.subscription_key, queue_settings)
|
70
75
|
subscribe_to_exchange
|
71
76
|
end
|
72
77
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Subscriber
|
5
|
-
attr_accessor :klass, :action, :attrs, :settings
|
5
|
+
attr_accessor :klass, :action, :attrs, :settings, :identifiers
|
6
6
|
attr_reader :payload
|
7
7
|
|
8
8
|
# @param settings: (Hash) { id: :id, direct_mode: false,
|
@@ -14,6 +14,7 @@ module PubSubModelSync
|
|
14
14
|
@action = action
|
15
15
|
@attrs = attrs
|
16
16
|
@settings = def_settings.merge(settings)
|
17
|
+
@identifiers = Array(settings[:id]).map(&:to_sym)
|
17
18
|
end
|
18
19
|
|
19
20
|
def process!(payload)
|
@@ -55,12 +56,11 @@ module PubSubModelSync
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def model_identifiers
|
58
|
-
identifiers = Array(settings[:id])
|
59
59
|
identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
|
60
60
|
end
|
61
61
|
|
62
62
|
def populate_model(model)
|
63
|
-
values = payload.data.slice(*attrs)
|
63
|
+
values = payload.data.slice(*attrs).except(*identifiers)
|
64
64
|
values.each do |attr, value|
|
65
65
|
model.send("#{attr}=", value)
|
66
66
|
end
|
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.5.
|
4
|
+
version: 0.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Owen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|