cloudenvoy 0.1.0.dev → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +41 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +32 -0
  7. data/Gemfile.lock +215 -1
  8. data/README.md +581 -7
  9. data/app/controllers/cloudenvoy/application_controller.rb +8 -0
  10. data/app/controllers/cloudenvoy/subscriber_controller.rb +59 -0
  11. data/cloudenvoy.gemspec +15 -2
  12. data/config/routes.rb +5 -0
  13. data/examples/rails/.ruby-version +1 -0
  14. data/examples/rails/Gemfile +15 -0
  15. data/examples/rails/Gemfile.lock +207 -0
  16. data/examples/rails/Procfile +1 -0
  17. data/examples/rails/README.md +31 -0
  18. data/examples/rails/Rakefile +8 -0
  19. data/examples/rails/app/assets/config/manifest.js +2 -0
  20. data/examples/rails/app/assets/images/.keep +0 -0
  21. data/examples/rails/app/assets/stylesheets/application.css +15 -0
  22. data/examples/rails/app/channels/application_cable/channel.rb +6 -0
  23. data/examples/rails/app/channels/application_cable/connection.rb +6 -0
  24. data/examples/rails/app/controllers/application_controller.rb +4 -0
  25. data/examples/rails/app/controllers/concerns/.keep +0 -0
  26. data/examples/rails/app/helpers/application_helper.rb +4 -0
  27. data/examples/rails/app/javascript/packs/application.js +15 -0
  28. data/examples/rails/app/jobs/application_job.rb +9 -0
  29. data/examples/rails/app/mailers/application_mailer.rb +6 -0
  30. data/examples/rails/app/models/application_record.rb +5 -0
  31. data/examples/rails/app/models/concerns/.keep +0 -0
  32. data/examples/rails/app/publishers/hello_publisher.rb +34 -0
  33. data/examples/rails/app/subscribers/hello_subscriber.rb +16 -0
  34. data/examples/rails/app/views/layouts/application.html.erb +14 -0
  35. data/examples/rails/app/views/layouts/mailer.html.erb +13 -0
  36. data/examples/rails/app/views/layouts/mailer.text.erb +1 -0
  37. data/examples/rails/bin/rails +6 -0
  38. data/examples/rails/bin/rake +6 -0
  39. data/examples/rails/bin/setup +35 -0
  40. data/examples/rails/config.ru +7 -0
  41. data/examples/rails/config/application.rb +19 -0
  42. data/examples/rails/config/boot.rb +7 -0
  43. data/examples/rails/config/cable.yml +10 -0
  44. data/examples/rails/config/credentials.yml.enc +1 -0
  45. data/examples/rails/config/database.yml +25 -0
  46. data/examples/rails/config/environment.rb +7 -0
  47. data/examples/rails/config/environments/development.rb +65 -0
  48. data/examples/rails/config/environments/production.rb +114 -0
  49. data/examples/rails/config/environments/test.rb +50 -0
  50. data/examples/rails/config/initializers/application_controller_renderer.rb +9 -0
  51. data/examples/rails/config/initializers/assets.rb +14 -0
  52. data/examples/rails/config/initializers/backtrace_silencers.rb +8 -0
  53. data/examples/rails/config/initializers/cloudenvoy.rb +22 -0
  54. data/examples/rails/config/initializers/content_security_policy.rb +29 -0
  55. data/examples/rails/config/initializers/cookies_serializer.rb +7 -0
  56. data/examples/rails/config/initializers/filter_parameter_logging.rb +6 -0
  57. data/examples/rails/config/initializers/inflections.rb +17 -0
  58. data/examples/rails/config/initializers/mime_types.rb +5 -0
  59. data/examples/rails/config/initializers/wrap_parameters.rb +16 -0
  60. data/examples/rails/config/locales/en.yml +33 -0
  61. data/examples/rails/config/master.key +1 -0
  62. data/examples/rails/config/puma.rb +37 -0
  63. data/examples/rails/config/routes.rb +4 -0
  64. data/examples/rails/config/spring.rb +8 -0
  65. data/examples/rails/config/storage.yml +34 -0
  66. data/examples/rails/db/development.sqlite3 +0 -0
  67. data/examples/rails/db/test.sqlite3 +0 -0
  68. data/examples/rails/lib/assets/.keep +0 -0
  69. data/examples/rails/log/.keep +0 -0
  70. data/examples/rails/public/404.html +67 -0
  71. data/examples/rails/public/422.html +67 -0
  72. data/examples/rails/public/500.html +66 -0
  73. data/examples/rails/public/apple-touch-icon-precomposed.png +0 -0
  74. data/examples/rails/public/apple-touch-icon.png +0 -0
  75. data/examples/rails/public/favicon.ico +0 -0
  76. data/examples/rails/storage/.keep +0 -0
  77. data/gemfiles/rails_5.2.gemfile +7 -0
  78. data/gemfiles/rails_5.2.gemfile.lock +251 -0
  79. data/gemfiles/rails_6.0.gemfile +7 -0
  80. data/gemfiles/rails_6.0.gemfile.lock +267 -0
  81. data/gemfiles/semantic_logger_3.4.gemfile +7 -0
  82. data/gemfiles/semantic_logger_3.4.gemfile.lock +265 -0
  83. data/gemfiles/semantic_logger_4.6.gemfile +7 -0
  84. data/gemfiles/semantic_logger_4.6.gemfile.lock +265 -0
  85. data/gemfiles/semantic_logger_4.7.0.gemfile +7 -0
  86. data/gemfiles/semantic_logger_4.7.0.gemfile.lock +265 -0
  87. data/gemfiles/semantic_logger_4.7.2.gemfile +7 -0
  88. data/gemfiles/semantic_logger_4.7.2.gemfile.lock +265 -0
  89. data/lib/cloudenvoy.rb +96 -2
  90. data/lib/cloudenvoy/authentication_error.rb +6 -0
  91. data/lib/cloudenvoy/authenticator.rb +57 -0
  92. data/lib/cloudenvoy/backend/google_pub_sub.rb +146 -0
  93. data/lib/cloudenvoy/backend/memory_pub_sub.rb +89 -0
  94. data/lib/cloudenvoy/config.rb +165 -0
  95. data/lib/cloudenvoy/engine.rb +20 -0
  96. data/lib/cloudenvoy/invalid_subscriber_error.rb +6 -0
  97. data/lib/cloudenvoy/logger_wrapper.rb +167 -0
  98. data/lib/cloudenvoy/message.rb +96 -0
  99. data/lib/cloudenvoy/middleware/chain.rb +250 -0
  100. data/lib/cloudenvoy/pub_sub_client.rb +76 -0
  101. data/lib/cloudenvoy/publisher.rb +211 -0
  102. data/lib/cloudenvoy/publisher_logger.rb +32 -0
  103. data/lib/cloudenvoy/subscriber.rb +222 -0
  104. data/lib/cloudenvoy/subscriber_logger.rb +26 -0
  105. data/lib/cloudenvoy/subscription.rb +19 -0
  106. data/lib/cloudenvoy/testing.rb +106 -0
  107. data/lib/cloudenvoy/topic.rb +19 -0
  108. data/lib/cloudenvoy/version.rb +1 -1
  109. data/lib/tasks/cloudenvoy.rake +61 -0
  110. metadata +263 -6
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # CLoudenvoy Rails engine
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Cloudenvoy
7
+
8
+ initializer 'cloudenvoy', before: :load_config_initializers do
9
+ Rails.application.routes.append do
10
+ mount Cloudenvoy::Engine, at: '/cloudenvoy'
11
+ end
12
+ end
13
+
14
+ config.generators do |g|
15
+ g.test_framework :rspec, fixture: false
16
+ g.assets false
17
+ g.helper false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ class InvalidSubscriberError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # Add contextual information to logs generated
5
+ # by subscribers/publishers.
6
+ #
7
+ # This class is a base class which aims at being inherited by
8
+ # object-specific logger wrappers. See Cloudenvoy::SubscriberLogger
9
+ # and Cloudenvoy::PublisherLogger.
10
+ class LoggerWrapper
11
+ attr_accessor :loggable
12
+
13
+ class << self
14
+ attr_accessor :log_context_processor
15
+ end
16
+
17
+ #
18
+ # The default context processor. Aims at being overriden by
19
+ # child classes.
20
+ #
21
+ # @return [Proc] The context processor proc.
22
+ #
23
+ def self.default_context_processor
24
+ @default_context_processor ||= ->(_) { {} }
25
+ end
26
+
27
+ #
28
+ # Build a new instance of the class.
29
+ #
30
+ # @param [Any] loggable The loggable to wrap for logging.
31
+ #
32
+ def initialize(loggable)
33
+ @loggable = loggable
34
+ end
35
+
36
+ #
37
+ # Return the Proc responsible for formatting the log payload.
38
+ #
39
+ # @return [Proc] The context processor.
40
+ #
41
+ def context_processor
42
+ @context_processor ||= loggable.class.cloudenvoy_options_hash[:log_context_processor] ||
43
+ self.class.log_context_processor ||
44
+ self.class.default_context_processor
45
+ end
46
+
47
+ #
48
+ # The block to pass to log messages.
49
+ #
50
+ # @return [Proc] The log block.
51
+ #
52
+ def log_block
53
+ @log_block ||= proc { context_processor.call(loggable) }
54
+ end
55
+
56
+ #
57
+ # Return the Cloudenvoy logger.
58
+ #
59
+ # @return [Logger, any] The cloudenvoy logger.
60
+ #
61
+ def logger
62
+ Cloudenvoy.logger
63
+ end
64
+
65
+ #
66
+ # Format main log message.
67
+ #
68
+ # @param [String] msg The message to log.
69
+ #
70
+ # @return [String] The formatted log message
71
+ #
72
+ def formatted_message(msg)
73
+ "[Cloudenvoy][#{loggable.class}] #{msg}"
74
+ end
75
+
76
+ #
77
+ # Log an info message.
78
+ #
79
+ # @param [String] msg The message to log.
80
+ # @param [Proc] &block Optional context block.
81
+ #
82
+ def info(msg, &block)
83
+ log_message(:info, msg, &block)
84
+ end
85
+
86
+ #
87
+ # Log an error message.
88
+ #
89
+ # @param [String] msg The message to log.
90
+ # @param [Proc] &block Optional context block.
91
+ #
92
+ def error(msg, &block)
93
+ log_message(:error, msg, &block)
94
+ end
95
+
96
+ #
97
+ # Log an fatal message.
98
+ #
99
+ # @param [String] msg The message to log.
100
+ # @param [Proc] &block Optional context block.
101
+ #
102
+ def fatal(msg, &block)
103
+ log_message(:fatal, msg, &block)
104
+ end
105
+
106
+ #
107
+ # Log an debut message.
108
+ #
109
+ # @param [String] msg The message to log.
110
+ # @param [Proc] &block Optional context block.
111
+ #
112
+ def debug(msg, &block)
113
+ log_message(:debug, msg, &block)
114
+ end
115
+
116
+ #
117
+ # Delegate all methods to the underlying logger.
118
+ #
119
+ # @param [String, Symbol] name The method to delegate.
120
+ # @param [Array<any>] *args The list of method arguments.
121
+ # @param [Proc] &block Block passed to the method.
122
+ #
123
+ # @return [Any] The method return value
124
+ #
125
+ def method_missing(name, *args, &block)
126
+ if logger.respond_to?(name)
127
+ logger.send(name, *args, &block)
128
+ else
129
+ super
130
+ end
131
+ end
132
+
133
+ #
134
+ # Check if the class respond to a certain method.
135
+ #
136
+ # @param [String, Symbol] name The name of the method.
137
+ # @param [Boolean] include_private Whether to check private methods or not. Default to false.
138
+ #
139
+ # @return [Boolean] Return true if the class respond to this method.
140
+ #
141
+ def respond_to_missing?(name, include_private = false)
142
+ logger.respond_to?(name) || super
143
+ end
144
+
145
+ private
146
+
147
+ #
148
+ # Log a message for the provided log level.
149
+ #
150
+ # @param [String, Symbol] level The log level
151
+ # @param [String] msg The message to log.
152
+ # @param [Proc] &block Optional context block.
153
+ #
154
+ def log_message(level, msg, &block)
155
+ # Merge log-specific context into object-specific context
156
+ payload_block = ->(*_args) { log_block.call.merge(block&.call || {}) }
157
+
158
+ # ActiveSupport::Logger does not support passing a payload through a block on top
159
+ # of a message.
160
+ if defined?(ActiveSupport::Logger) && logger.is_a?(ActiveSupport::Logger)
161
+ logger.send(level) { "#{formatted_message(msg)} -- #{payload_block.call}" }
162
+ else
163
+ logger.send(level, formatted_message(msg), &payload_block)
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ # Represents a Pub/Sub message
5
+ class Message
6
+ attr_writer :topic
7
+ attr_accessor :id, :payload, :metadata, :sub_uri
8
+
9
+ #
10
+ # Return an instantiated message from a Pub/Sub webhook
11
+ # payload.
12
+ #
13
+ # @param [Hash] input_payload The Pub/Sub webhook hash describing
14
+ # the message to process.
15
+ #
16
+ # @return [Cloudenvoy::Message] The instantiated message.
17
+ #
18
+ def self.from_descriptor(input_payload)
19
+ # Build new message
20
+ new(
21
+ id: input_payload.dig('message', 'message_id'),
22
+ payload: JSON.parse(Base64.decode64(input_payload.dig('message', 'data'))),
23
+ metadata: input_payload.dig('message', 'attributes'),
24
+ sub_uri: input_payload['subscription']
25
+ )
26
+ end
27
+
28
+ #
29
+ # Constructor
30
+ #
31
+ # @param [String] id The message ID
32
+ # @param [Hash, String] payload The message payload
33
+ # @param [Hash] metadata The message attributes
34
+ # @param [String] topic The topic - will be inferred from sub_uri if left blank
35
+ # @param [String] sub_uri The sub_uri this message was sent for
36
+ #
37
+ def initialize(id: nil, payload: nil, metadata: nil, topic: nil, sub_uri: nil)
38
+ @id = id
39
+ @payload = payload
40
+ @topic = topic
41
+ @metadata = metadata || {}
42
+ @sub_uri = sub_uri
43
+ end
44
+
45
+ #
46
+ # Return the message topic.
47
+ #
48
+ # @return [String] The message topic.
49
+ #
50
+ def topic
51
+ return @topic if @topic
52
+ return nil unless sub_uri
53
+
54
+ Subscriber.parse_sub_uri(sub_uri)[1]
55
+ end
56
+
57
+ #
58
+ # Return the instantiated Subscriber designated to process this message.
59
+ #
60
+ # @return [Subscriber] The instantiated subscriber.
61
+ #
62
+ def subscriber
63
+ @subscriber ||= begin
64
+ return nil unless sub_uri && (klass = Subscriber.from_sub_uri(sub_uri))
65
+
66
+ klass.new(message: self)
67
+ end
68
+ end
69
+
70
+ #
71
+ # Return a hash description of the message.
72
+ #
73
+ # @return [Hash] The message description
74
+ #
75
+ def to_h
76
+ {
77
+ id: id,
78
+ payload: payload,
79
+ metadata: metadata,
80
+ topic: topic,
81
+ sub_uri: sub_uri
82
+ }.compact
83
+ end
84
+
85
+ #
86
+ # Equality operator.
87
+ #
88
+ # @param [Any] other The object to compare.
89
+ #
90
+ # @return [Boolean] True if the object is equal.
91
+ #
92
+ def ==(other)
93
+ other.is_a?(self.class) && other.id == id
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudenvoy
4
+ module Middleware
5
+ # The class below was originally taken from Sidekiq.
6
+ # See: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/middleware/chain.rb
7
+ #
8
+ # Middleware are callables configured to run before/after a message is processed.
9
+ # Middlewares can be configured to run on the client side (when jobs are pushed
10
+ # to Cloud Tasks) as well as on the server side (when jobs are processed by
11
+ # your application)
12
+ #
13
+ # To add a middleware on publishers:
14
+ #
15
+ # Cloudenvoy.configure do |config|
16
+ # config.publisher_middleware do |chain|
17
+ # chain.add MyPublisherHook
18
+ # end
19
+ # end
20
+ #
21
+ # To modify middlewares on subscribers, just call
22
+ # with another block:
23
+ #
24
+ # Cloudenvoy.configure do |config|
25
+ # config.subscriber_middleware do |chain|
26
+ # chain.add MySubscriberHook
27
+ # chain.remove ActiveRecord
28
+ # end
29
+ # end
30
+ #
31
+ # To insert immediately preceding another entry:
32
+ #
33
+ # Cloudenvoy.configure do |config|
34
+ # config.publisher_middleware do |chain|
35
+ # chain.insert_before ActiveRecord, MyPublisherHook
36
+ # end
37
+ # end
38
+ #
39
+ # To insert immediately after another entry:
40
+ #
41
+ # Cloudenvoy.configure do |config|
42
+ # config.publisher_middleware do |chain|
43
+ # chain.insert_after ActiveRecord, MyPublisherHook
44
+ # end
45
+ # end
46
+ #
47
+ # This is an example of a minimal server middleware:
48
+ #
49
+ # class MySubscriberHook
50
+ # def call(subscriber, msg, queue)
51
+ # puts "Before work"
52
+ # yield
53
+ # puts "After work"
54
+ # end
55
+ # end
56
+ #
57
+ # This is an example of a minimal client middleware, note
58
+ # the method must return the result or the job will not push
59
+ # to Redis:
60
+ #
61
+ # class MyPublisherHook
62
+ # def call(publisher, msg, queue, redis_pool)
63
+ # puts "Before push"
64
+ # result = yield
65
+ # puts "After push"
66
+ # result
67
+ # end
68
+ # end
69
+ #
70
+ class Chain
71
+ include Enumerable
72
+
73
+ #
74
+ # Build a new middleware chain.
75
+ #
76
+ def initialize
77
+ @entries = nil
78
+ yield self if block_given?
79
+ end
80
+
81
+ #
82
+ # Iterate over the list middlewares and execute the block on each item.
83
+ #
84
+ # @param [Proc] &block The block to execute on each item.
85
+ #
86
+ def each(&block)
87
+ entries.each(&block)
88
+ end
89
+
90
+ #
91
+ # Return the list of middlewares.
92
+ #
93
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The list of middlewares
94
+ #
95
+ def entries
96
+ @entries ||= []
97
+ end
98
+
99
+ #
100
+ # Remove a middleware from the list.
101
+ #
102
+ # @param [Class] klass The middleware class to remove.
103
+ #
104
+ def remove(klass)
105
+ entries.delete_if { |entry| entry.klass == klass }
106
+ end
107
+
108
+ #
109
+ # Add a middleware at the end of the list.
110
+ #
111
+ # @param [Class] klass The middleware class to add.
112
+ # @param [Arry<any>] *args The list of arguments to the middleware.
113
+ #
114
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
115
+ #
116
+ def add(klass, *args)
117
+ remove(klass) if exists?(klass)
118
+ entries << Entry.new(klass, *args)
119
+ end
120
+
121
+ #
122
+ # Add a middleware at the beginning of the list.
123
+ #
124
+ # @param [Class] klass The middleware class to add.
125
+ # @param [Arry<any>] *args The list of arguments to the middleware.
126
+ #
127
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
128
+ #
129
+ def prepend(klass, *args)
130
+ remove(klass) if exists?(klass)
131
+ entries.insert(0, Entry.new(klass, *args))
132
+ end
133
+
134
+ #
135
+ # Add a middleware before another middleware.
136
+ #
137
+ # @param [Class] oldklass The middleware class before which the new middleware should be inserted.
138
+ # @param [Class] newklass The middleware class to insert.
139
+ # @param [Arry<any>] *args The list of arguments for the inserted middleware.
140
+ #
141
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
142
+ #
143
+ def insert_before(oldklass, newklass, *args)
144
+ i = entries.index { |entry| entry.klass == newklass }
145
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
146
+ i = entries.index { |entry| entry.klass == oldklass } || 0
147
+ entries.insert(i, new_entry)
148
+ end
149
+
150
+ #
151
+ # Add a middleware after another middleware.
152
+ #
153
+ # @param [Class] oldklass The middleware class after which the new middleware should be inserted.
154
+ # @param [Class] newklass The middleware class to insert.
155
+ # @param [Arry<any>] *args The list of arguments for the inserted middleware.
156
+ #
157
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
158
+ #
159
+ def insert_after(oldklass, newklass, *args)
160
+ i = entries.index { |entry| entry.klass == newklass }
161
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
162
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
163
+ entries.insert(i + 1, new_entry)
164
+ end
165
+
166
+ #
167
+ # Checks if middleware has been added to the list.
168
+ #
169
+ # @param [Class] klass The middleware class to check.
170
+ #
171
+ # @return [Boolean] Return true if the middleware is in the list.
172
+ #
173
+ def exists?(klass)
174
+ any? { |entry| entry.klass == klass }
175
+ end
176
+
177
+ #
178
+ # Checks if the middlware list is empty
179
+ #
180
+ # @return [Boolean] Return true if the middleware list is empty.
181
+ #
182
+ def empty?
183
+ @entries.nil? || @entries.empty?
184
+ end
185
+
186
+ #
187
+ # Return a list of instantiated middlewares. Each middleware gets
188
+ # initialize with the args originally passed to `add`, `insert_before` etc.
189
+ #
190
+ # @return [Array<any>] The list of instantiated middlewares.
191
+ #
192
+ def retrieve
193
+ map(&:make_new)
194
+ end
195
+
196
+ #
197
+ # Empty the list of middlewares.
198
+ #
199
+ # @return [Array<Cloudenvoy::Middleware::Chain::Entry>] The updated list of middlewares
200
+ #
201
+ def clear
202
+ entries.clear
203
+ end
204
+
205
+ #
206
+ # Invoke the chain of middlewares.
207
+ #
208
+ # @param [Array<any>] *args The args to pass to each middleware.
209
+ #
210
+ def invoke(*args)
211
+ return yield if empty?
212
+
213
+ chain = retrieve.dup
214
+ traverse_chain = lambda do
215
+ if chain.empty?
216
+ yield
217
+ else
218
+ chain.shift.call(*args, &traverse_chain)
219
+ end
220
+ end
221
+ traverse_chain.call
222
+ end
223
+ end
224
+
225
+ # Middleware list item.
226
+ class Entry
227
+ attr_reader :klass, :args
228
+
229
+ #
230
+ # Build a new entry.
231
+ #
232
+ # @param [Class] klass The middleware class.
233
+ # @param [Array<any>] *args The list of arguments for the middleware.
234
+ #
235
+ def initialize(klass, *args)
236
+ @klass = klass
237
+ @args = args
238
+ end
239
+
240
+ #
241
+ # Return an instantiated middleware.
242
+ #
243
+ # @return [Any] The instantiated middleware.
244
+ #
245
+ def make_new
246
+ @klass.new(*@args)
247
+ end
248
+ end
249
+ end
250
+ end