cloudenvoy 0.1.0.dev → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +37 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Appraisals +9 -0
  6. data/CHANGELOG.md +4 -0
  7. data/Gemfile.lock +212 -1
  8. data/README.md +569 -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 +14 -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 +248 -0
  79. data/gemfiles/rails_6.0.gemfile +7 -0
  80. data/gemfiles/rails_6.0.gemfile.lock +264 -0
  81. data/lib/cloudenvoy.rb +96 -2
  82. data/lib/cloudenvoy/authentication_error.rb +6 -0
  83. data/lib/cloudenvoy/authenticator.rb +57 -0
  84. data/lib/cloudenvoy/backend/google_pub_sub.rb +110 -0
  85. data/lib/cloudenvoy/backend/memory_pub_sub.rb +88 -0
  86. data/lib/cloudenvoy/config.rb +165 -0
  87. data/lib/cloudenvoy/engine.rb +20 -0
  88. data/lib/cloudenvoy/invalid_subscriber_error.rb +6 -0
  89. data/lib/cloudenvoy/logger_wrapper.rb +167 -0
  90. data/lib/cloudenvoy/message.rb +96 -0
  91. data/lib/cloudenvoy/middleware/chain.rb +250 -0
  92. data/lib/cloudenvoy/pub_sub_client.rb +62 -0
  93. data/lib/cloudenvoy/publisher.rb +211 -0
  94. data/lib/cloudenvoy/publisher_logger.rb +32 -0
  95. data/lib/cloudenvoy/subscriber.rb +218 -0
  96. data/lib/cloudenvoy/subscriber_logger.rb +26 -0
  97. data/lib/cloudenvoy/subscription.rb +19 -0
  98. data/lib/cloudenvoy/testing.rb +106 -0
  99. data/lib/cloudenvoy/topic.rb +19 -0
  100. data/lib/cloudenvoy/version.rb +1 -1
  101. data/lib/tasks/cloudenvoy.rake +61 -0
  102. metadata +241 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd2ec9d1e57064ce857c167a6ec7059790daa1f6c55dfab0131ba29bc133b2db
4
- data.tar.gz: c15723602a03899edd1abe13bab834609af75ce5dc6eb8487640bfcd1aa56a2f
3
+ metadata.gz: 788d59dcdb276ba209ea7502f1d03765ab0beac646089ac5891ea67967a96a25
4
+ data.tar.gz: 819add5f2b7d7b3b37fc6f1f32d008af05fddd1409d585e0a88c7249f0427f57
5
5
  SHA512:
6
- metadata.gz: 2f6a835095c6b26d0601a9e244a1b9f4c1262f5ef0f42c7ae9a3b5b8e3d04a9618b8fd8cfef06b419cabb1e11a85d64986b56dbac07b3b1f20f40623a26877d6
7
- data.tar.gz: 6de2c1aa9543cd354e6a304c63f7ceebd97920d68c4f5241f7b478e7887067558f19d24360c1546f8b49e94238ce2eae4e61a227c4d13f94c9a1464ded34a880
6
+ metadata.gz: 3df6bb839c332feea426ec26ac09f46c951f8997159a53d32ce5ac3a11549648842f747ae1b2a0e962f399d8d12a11e6a64501d72134c0347485d39ec07d0ca8
7
+ data.tar.gz: cd320825e2b8c842e2746c5b7acbfbb82751f743c9bae38a4e997f411a3aa44b6d1e658bf8c8a7afba108ecc7fcc3a5cca4bd2c4b9711a9bfeadd1d88e512c31
@@ -0,0 +1,37 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby:
15
+ - '2.5.x'
16
+ - '2.6.x'
17
+ appraisal:
18
+ - 'rails-5.2'
19
+ - 'rails-6.0'
20
+ steps:
21
+ - name: Setup System
22
+ run: sudo apt-get install libsqlite3-dev
23
+ - uses: actions/checkout@v2
24
+ - uses: zhulik/redis-action@1.1.0
25
+ - name: Set up Ruby 2.6
26
+ uses: actions/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ - name: Build and test with Rake
30
+ env:
31
+ APPRAISAL_CONTEXT: ${{ matrix.appraisal }}
32
+ run: |
33
+ gem install bundler
34
+ bundle install --jobs 4 --retry 3
35
+ bundle exec rubocop
36
+ bundle exec appraisal ${APPRAISAL_CONTEXT} bundle
37
+ bundle exec appraisal ${APPRAISAL_CONTEXT} rspec
data/.gitignore CHANGED
@@ -3,9 +3,12 @@
3
3
  /_yardoc/
4
4
  /coverage/
5
5
  /doc/
6
+ /examples/rails/log/*.log
7
+ /examples/rails/tmp/
6
8
  /pkg/
7
9
  /spec/reports/
8
10
  /tmp/
11
+ /log/*.log
9
12
 
10
13
  # rspec failure tracking
11
14
  .rspec_status
@@ -32,6 +32,7 @@ RSpec/ScatteredSetup:
32
32
  Metrics/BlockLength:
33
33
  Exclude:
34
34
  - cloudenvoy.gemspec
35
+ - lib/tasks/**/*
35
36
  - 'spec/**/*'
36
37
 
37
38
  Style/Documentation:
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'rails-5.2' do
4
+ gem 'rails', '5.2'
5
+ end
6
+
7
+ appraise 'rails-6.0' do
8
+ gem 'rails', '6.0'
9
+ end
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## [v0.1.0](https://github.com/keypup-io/cloudenvoy/tree/v0.1.0) (2020-09-16)
4
+ Initial release
@@ -1,19 +1,189 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cloudenvoy (0.1.0.dev)
4
+ cloudenvoy (0.1.0)
5
+ activesupport
6
+ google-cloud-pubsub (~> 2.0)
7
+ jwt
8
+ retriable
5
9
 
6
10
  GEM
7
11
  remote: https://rubygems.org/
8
12
  specs:
13
+ actioncable (6.0.3.2)
14
+ actionpack (= 6.0.3.2)
15
+ nio4r (~> 2.0)
16
+ websocket-driver (>= 0.6.1)
17
+ actionmailbox (6.0.3.2)
18
+ actionpack (= 6.0.3.2)
19
+ activejob (= 6.0.3.2)
20
+ activerecord (= 6.0.3.2)
21
+ activestorage (= 6.0.3.2)
22
+ activesupport (= 6.0.3.2)
23
+ mail (>= 2.7.1)
24
+ actionmailer (6.0.3.2)
25
+ actionpack (= 6.0.3.2)
26
+ actionview (= 6.0.3.2)
27
+ activejob (= 6.0.3.2)
28
+ mail (~> 2.5, >= 2.5.4)
29
+ rails-dom-testing (~> 2.0)
30
+ actionpack (6.0.3.2)
31
+ actionview (= 6.0.3.2)
32
+ activesupport (= 6.0.3.2)
33
+ rack (~> 2.0, >= 2.0.8)
34
+ rack-test (>= 0.6.3)
35
+ rails-dom-testing (~> 2.0)
36
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
37
+ actiontext (6.0.3.2)
38
+ actionpack (= 6.0.3.2)
39
+ activerecord (= 6.0.3.2)
40
+ activestorage (= 6.0.3.2)
41
+ activesupport (= 6.0.3.2)
42
+ nokogiri (>= 1.8.5)
43
+ actionview (6.0.3.2)
44
+ activesupport (= 6.0.3.2)
45
+ builder (~> 3.1)
46
+ erubi (~> 1.4)
47
+ rails-dom-testing (~> 2.0)
48
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
49
+ activejob (6.0.3.2)
50
+ activesupport (= 6.0.3.2)
51
+ globalid (>= 0.3.6)
52
+ activemodel (6.0.3.2)
53
+ activesupport (= 6.0.3.2)
54
+ activerecord (6.0.3.2)
55
+ activemodel (= 6.0.3.2)
56
+ activesupport (= 6.0.3.2)
57
+ activestorage (6.0.3.2)
58
+ actionpack (= 6.0.3.2)
59
+ activejob (= 6.0.3.2)
60
+ activerecord (= 6.0.3.2)
61
+ marcel (~> 0.3.1)
62
+ activesupport (6.0.3.2)
63
+ concurrent-ruby (~> 1.0, >= 1.0.2)
64
+ i18n (>= 0.7, < 2)
65
+ minitest (~> 5.1)
66
+ tzinfo (~> 1.1)
67
+ zeitwerk (~> 2.2, >= 2.2.2)
68
+ addressable (2.7.0)
69
+ public_suffix (>= 2.0.2, < 5.0)
70
+ appraisal (2.3.0)
71
+ bundler
72
+ rake
73
+ thor (>= 0.14.0)
9
74
  ast (2.4.1)
75
+ builder (3.2.4)
76
+ concurrent-ruby (1.1.6)
77
+ crack (0.4.3)
78
+ safe_yaml (~> 1.0.0)
79
+ crass (1.0.6)
10
80
  diff-lcs (1.4.2)
81
+ erubi (1.9.0)
82
+ faraday (1.0.1)
83
+ multipart-post (>= 1.2, < 3)
84
+ gapic-common (0.3.4)
85
+ google-protobuf (~> 3.12, >= 3.12.2)
86
+ googleapis-common-protos (>= 1.3.9, < 2.0)
87
+ googleapis-common-protos-types (>= 1.0.4, < 2.0)
88
+ googleauth (~> 0.9)
89
+ grpc (~> 1.25)
90
+ globalid (0.4.2)
91
+ activesupport (>= 4.2.0)
92
+ google-cloud-core (1.5.0)
93
+ google-cloud-env (~> 1.0)
94
+ google-cloud-errors (~> 1.0)
95
+ google-cloud-env (1.3.3)
96
+ faraday (>= 0.17.3, < 2.0)
97
+ google-cloud-errors (1.0.1)
98
+ google-cloud-pubsub (2.0.0)
99
+ concurrent-ruby (~> 1.1)
100
+ google-cloud-core (~> 1.5)
101
+ google-cloud-pubsub-v1 (~> 0.0)
102
+ google-cloud-pubsub-v1 (0.1.2)
103
+ gapic-common (~> 0.3)
104
+ google-cloud-errors (~> 1.0)
105
+ grpc-google-iam-v1 (>= 0.6.10, < 2.0)
106
+ google-protobuf (3.13.0-universal-darwin)
107
+ googleapis-common-protos (1.3.10)
108
+ google-protobuf (~> 3.11)
109
+ googleapis-common-protos-types (>= 1.0.5, < 2.0)
110
+ grpc (~> 1.27)
111
+ googleapis-common-protos-types (1.0.5)
112
+ google-protobuf (~> 3.11)
113
+ googleauth (0.13.1)
114
+ faraday (>= 0.17.3, < 2.0)
115
+ jwt (>= 1.4, < 3.0)
116
+ memoist (~> 0.16)
117
+ multi_json (~> 1.11)
118
+ os (>= 0.9, < 2.0)
119
+ signet (~> 0.14)
120
+ grpc (1.32.0-universal-darwin)
121
+ google-protobuf (~> 3.13)
122
+ googleapis-common-protos-types (~> 1.0)
123
+ grpc-google-iam-v1 (0.6.10)
124
+ google-protobuf (~> 3.11)
125
+ googleapis-common-protos (>= 1.3.10, < 2.0)
126
+ grpc (~> 1.27)
127
+ hashdiff (1.0.1)
128
+ i18n (1.8.5)
129
+ concurrent-ruby (~> 1.0)
11
130
  jaro_winkler (1.5.4)
131
+ jwt (2.2.2)
132
+ loofah (2.6.0)
133
+ crass (~> 1.0.2)
134
+ nokogiri (>= 1.5.9)
135
+ mail (2.7.1)
136
+ mini_mime (>= 0.1.1)
137
+ marcel (0.3.3)
138
+ mimemagic (~> 0.3.2)
139
+ memoist (0.16.2)
140
+ method_source (1.0.0)
141
+ mimemagic (0.3.5)
142
+ mini_mime (1.0.2)
143
+ mini_portile2 (2.4.0)
144
+ minitest (5.14.1)
145
+ multi_json (1.15.0)
146
+ multipart-post (2.1.1)
147
+ nio4r (2.5.2)
148
+ nokogiri (1.10.10)
149
+ mini_portile2 (~> 2.4.0)
150
+ os (1.1.1)
12
151
  parallel (1.19.2)
13
152
  parser (2.7.1.4)
14
153
  ast (~> 2.4.1)
154
+ public_suffix (4.0.5)
155
+ rack (2.2.3)
156
+ rack-test (1.1.0)
157
+ rack (>= 1.0, < 3)
158
+ rails (6.0.3.2)
159
+ actioncable (= 6.0.3.2)
160
+ actionmailbox (= 6.0.3.2)
161
+ actionmailer (= 6.0.3.2)
162
+ actionpack (= 6.0.3.2)
163
+ actiontext (= 6.0.3.2)
164
+ actionview (= 6.0.3.2)
165
+ activejob (= 6.0.3.2)
166
+ activemodel (= 6.0.3.2)
167
+ activerecord (= 6.0.3.2)
168
+ activestorage (= 6.0.3.2)
169
+ activesupport (= 6.0.3.2)
170
+ bundler (>= 1.3.0)
171
+ railties (= 6.0.3.2)
172
+ sprockets-rails (>= 2.0.0)
173
+ rails-dom-testing (2.0.3)
174
+ activesupport (>= 4.2.0)
175
+ nokogiri (>= 1.6)
176
+ rails-html-sanitizer (1.3.0)
177
+ loofah (~> 2.3)
178
+ railties (6.0.3.2)
179
+ actionpack (= 6.0.3.2)
180
+ activesupport (= 6.0.3.2)
181
+ method_source
182
+ rake (>= 0.8.7)
183
+ thor (>= 0.20.3, < 2.0)
15
184
  rainbow (3.0.0)
16
185
  rake (13.0.1)
186
+ retriable (3.1.2)
17
187
  rspec (3.9.0)
18
188
  rspec-core (~> 3.9.0)
19
189
  rspec-expectations (~> 3.9.0)
@@ -26,6 +196,14 @@ GEM
26
196
  rspec-mocks (3.9.1)
27
197
  diff-lcs (>= 1.2.0, < 2.0)
28
198
  rspec-support (~> 3.9.0)
199
+ rspec-rails (4.0.1)
200
+ actionpack (>= 4.2)
201
+ activesupport (>= 4.2)
202
+ railties (>= 4.2)
203
+ rspec-core (~> 3.9)
204
+ rspec-expectations (~> 3.9)
205
+ rspec-mocks (~> 3.9)
206
+ rspec-support (~> 3.9)
29
207
  rspec-support (3.9.3)
30
208
  rubocop (0.76.0)
31
209
  jaro_winkler (~> 1.5.1)
@@ -37,17 +215,50 @@ GEM
37
215
  rubocop-rspec (1.37.0)
38
216
  rubocop (>= 0.68.1)
39
217
  ruby-progressbar (1.10.1)
218
+ safe_yaml (1.0.5)
219
+ signet (0.14.0)
220
+ addressable (~> 2.3)
221
+ faraday (>= 0.17.3, < 2.0)
222
+ jwt (>= 1.5, < 3.0)
223
+ multi_json (~> 1.10)
224
+ sprockets (4.0.2)
225
+ concurrent-ruby (~> 1.0)
226
+ rack (> 1, < 3)
227
+ sprockets-rails (3.2.1)
228
+ actionpack (>= 4.0)
229
+ activesupport (>= 4.0)
230
+ sprockets (>= 3.0.0)
231
+ sqlite3 (1.4.2)
232
+ thor (1.0.1)
233
+ thread_safe (0.3.6)
234
+ timecop (0.9.1)
235
+ tzinfo (1.2.7)
236
+ thread_safe (~> 0.1)
40
237
  unicode-display_width (1.6.1)
238
+ webmock (3.8.3)
239
+ addressable (>= 2.3.6)
240
+ crack (>= 0.3.2)
241
+ hashdiff (>= 0.4.0, < 2.0.0)
242
+ websocket-driver (0.7.3)
243
+ websocket-extensions (>= 0.1.0)
244
+ websocket-extensions (0.1.5)
245
+ zeitwerk (2.4.0)
41
246
 
42
247
  PLATFORMS
43
248
  ruby
44
249
 
45
250
  DEPENDENCIES
251
+ appraisal
46
252
  cloudenvoy!
253
+ rails
47
254
  rake (>= 12.3.3)
48
255
  rspec (~> 3.0)
256
+ rspec-rails
49
257
  rubocop (= 0.76.0)
50
258
  rubocop-rspec (= 1.37.0)
259
+ sqlite3
260
+ timecop
261
+ webmock
51
262
 
52
263
  BUNDLED WITH
53
264
  2.1.4
data/README.md CHANGED
@@ -1,8 +1,33 @@
1
+ ![Build Status](https://github.com/keypup-io/cloudenvoy/workflows/Test/badge.svg) [![Gem Version](https://badge.fury.io/rb/cloudenvoy.svg)](https://badge.fury.io/rb/cloudenvoy)
2
+
3
+ **Note**: this gem is currently in alpha stage and has not been tested in production yet.
4
+
1
5
  # Cloudenvoy
2
6
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cloudenvoy`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+ Cross-application messaging framework for GCP Pub/Sub.
8
+
9
+ Cloudenvoy provides an easy to use interface to GCP Pub/Sub. Using Cloudenvoy you can simplify cross-application event messaging by using a publish/subscribe approach. Pub/Sub is particularly suited for micro-service architectures where a great number of components need to be aware of other components' activities. In these architectures using point to point communication via API can quickly become messy and hard to maintain due to the number of interconnections to maintain.
10
+
11
+ Pub/Sub solves that event distribution problem by allowing developers to define topics, publishers and subscribers to distribute and process event messages. Cloudenvoy furthers simplifies the process of setting up Pub/Sub by giving developers an object-oriented way of managing publishers and subscribers.
12
+
13
+ Cloudenvoy works with the local pub/sub emulator as well, meaning that you can work offline without access to GCP.
14
+
15
+ ## Summary
4
16
 
5
- TODO: Delete this and the text above, and describe your gem
17
+ 1. [Installation](#installation)
18
+ 2. [Get started with Rails](#get-started-with-rails)
19
+ 3. [Configuring Cloudenvoy](#configuring-cloudenvoy)
20
+ 1. [Pub/Sub authentication & permissions](#pubsub-authentication--permissions)
21
+ 2. [Cloudenvoy initializer](#cloudenvoy-initializer)
22
+ 4. [Creating topics and subscriptions](#creating-topics-and-subscriptions)
23
+ 1. [Sending messages](#sending-messages)
24
+ 2. [Publisher implementation layout](#publisher-implementation-layout)
25
+ 5. [Receiving messages](#receiving-messages)
26
+ 6. [Error Handling](#error-handling)
27
+ 7. [Testing](#testing)
28
+ 1. [Test helper setup](#test-helper-setup)
29
+ 2. [In-memory queues](#in-memory-queues)
30
+ 3. [Unit tests](#unit-tests)
6
31
 
7
32
  ## Installation
8
33
 
@@ -20,9 +45,547 @@ Or install it yourself as:
20
45
 
21
46
  $ gem install cloudenvoy
22
47
 
23
- ## Usage
48
+ ## Get started with Rails
49
+
50
+ Cloudenvoy is pre-integrated with Rails. Follow the steps below to get started.
51
+
52
+ Install the pub/sub local emulator
53
+ ```bash
54
+ gcloud components install pubsub-emulator
55
+ gcloud components update
56
+ ```
57
+
58
+ Add the following initializer
59
+ ```ruby
60
+ # config/initializers/cloudenvoy.rb
61
+
62
+ Cloudenvoy.configure do |config|
63
+ #
64
+ # GCP Configuration
65
+ #
66
+ config.gcp_project_id = 'some-project'
67
+ config.gcp_sub_prefix = 'my-app'
68
+
69
+ #
70
+ # Adapt the server port to be the one used by your Rails web process
71
+ #
72
+ config.processor_host = 'http://localhost:3000'
73
+
74
+ #
75
+ # If you do not have any Rails secret_key_base defined, uncomment the following
76
+ # This secret is used to authenticate messages sent to the processing endpoint
77
+ # of your application.
78
+ #
79
+ # config.secret = 'some-long-token'
80
+ end
81
+ ```
82
+
83
+ Define a publisher:
84
+ ```ruby
85
+ # app/publishers/dummy_publisher.rb
86
+
87
+ class DummyPublisher
88
+ include Cloudenvoy::Publisher
89
+
90
+ cloudenvoy_options topic: 'test-msgs'
91
+
92
+ # Format the message payload. The payload can be a hash
93
+ # or a string.
94
+ def payload(msg)
95
+ {
96
+ type: 'message',
97
+ content: msg
98
+ }
99
+ end
100
+ end
101
+ ```
102
+
103
+ Define a subscriber:
104
+ ```ruby
105
+ # app/subscribers/dummy_subscriber.rb
106
+
107
+ class HelloSubscriber
108
+ include Cloudenvoy::Subscriber
109
+
110
+ cloudenvoy_options topics: ['test-msgs']
111
+
112
+ # Do something with the message
113
+ def process(message)
114
+ logger.info("Received message #{message.payload.dig('content')}")
115
+ end
116
+ end
117
+ ```
118
+
119
+ Launch the pub/sub emulator:
120
+ ```bash
121
+ gcloud beta emulators pubsub start
122
+ ```
123
+
124
+ Use cloudenvoy to setup your topic and subscription
125
+ ```bash
126
+ bundle exec rake cloudenvoy:setup
127
+ ```
128
+
129
+ Launch Rails
130
+ ```bash
131
+ rails s -p 3000
132
+ ```
133
+
134
+ Open a Rails console and send a message
135
+ ```ruby
136
+ DummyPublisher.publish('Hello pub/sub')
137
+ ```
138
+
139
+ Your Rails logs should display the following:
140
+ ```log
141
+ Started POST "/cloudenvoy/receive?token=1234" for 66.102.6.140 at 2020-09-16 11:12:47 +0200
142
+ Processing by Cloudenvoy::SubscriberController#receive as JSON
143
+ Parameters: {"message"=>{"attributes"=>{"kind"=>"hello"}, "data"=>"eyJ0eXBlIjoibWVzc2FnZSIsImNvbnRlbnQiOiJIZWxsbyBmcmllbmQifQ==", "messageId"=>"1501653492745522", "message_id"=>"1501653492745522", "publishTime"=>"2020-09-16T09:12:45.214Z", "publish_time"=>"2020-09-16T09:12:45.214Z"}, "subscription"=>"projects/keypup-dev/subscriptions/my-app.hello_subscriber.test-msgs", "token"=>"eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDAyNDc0OTh9.5SbVsDCZLcyoFXseCpPuvE7KY7WXqIQtO6ceoFXcrdw", "subscriber"=>{"message"=>{"attributes"=>{"kind"=>"hello"}, "data"=>"eyJ0eXBlIjoibWVzc2FnZSIsImNvbnRlbnQiOiJIZWxsbyBmcmllbmQifQ==", "messageId"=>"1501653492745522", "message_id"=>"1501653492745522", "publishTime"=>"2020-09-16T09:12:45.214Z", "publish_time"=>"2020-09-16T09:12:45.214Z"}, "subscription"=>"projects/keypup-dev/subscriptions/my-app.hello_subscriber.test-msgs"}}
144
+ [Cloudenvoy][HelloSubscriber][1501653492745522] Processing message... -- {:id=>"1501653492745522", :metadata=>{}, :topic=>"test-msgs"}
145
+ [Cloudenvoy][HelloSubscriber][1501653492745522] Received message Hello pub/sub -- {:id=>"1501653492745522", :metadata=>{}, :topic=>"test-msgs"}
146
+ [Cloudenvoy][HelloSubscriber][1501653492745522] Processing done after 0.001s -- {:id=>"1501653492745522", :metadata=>{}, :topic=>"test-msgs", :duration=>0.001}
147
+ Completed 204 No Content in 1ms (ActiveRecord: 0.0ms | Allocations: 500)
148
+ ```
149
+
150
+ Hurray! Your published message was immediately processed by the subscriber.
151
+
152
+ ## Configuring Cloudenvoy
153
+
154
+ ### Pub/Sub authentication & permissions
155
+
156
+ The Google Cloud library authenticates via the Google Cloud SDK by default. If you do not have it setup then we recommend you [install it](https://cloud.google.com/sdk/docs/quickstarts).
157
+
158
+ Other options are available such as using a service account. You can see all authentication options in the [Google Cloud Authentication guide](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-bigquery/AUTHENTICATION.md).
159
+
160
+ In order to function properly Cloudenvoy requires the authenticated account to have the following IAM permissions:
161
+ - `pubsub.subscriptions.create`
162
+ - `pubsub.subscriptions.get`
163
+ - `pubsub.topics.create`
164
+ - `pubsub.topics.get`
165
+ - `pubsub.topics.publish`
166
+
167
+ To get started quickly you can add the `roles/pubsub.admin` role to your account via the [IAM Console](https://console.cloud.google.com/iam-admin/iam). This is not required if your account is a project admin account.
168
+
169
+ ### Cloudenvoy initializer
170
+
171
+ The gem can be configured through an initializer. See below all the available configuration options.
172
+
173
+ ```ruby
174
+ # config/initializers/cloudenvoy.rb
175
+
176
+ Cloudenvoy.configure do |config|
177
+ #
178
+ # If you do not have any Rails secret_key_base defined, uncomment the following.
179
+ # This secret is used to authenticate messages sent to the processing endpoint
180
+ # of your application.
181
+ #
182
+ # Default with Rails: Rails.application.credentials.secret_key_base
183
+ #
184
+ # config.secret = 'some-long-token'
185
+
186
+ #
187
+ # GCP Configuration
188
+ #
189
+ config.gcp_project_id = 'some-project'
190
+
191
+ #
192
+ # Specify the namespace for your subscriptions
193
+ #
194
+ # The gem attempts to keep GCP subscriptions organized by
195
+ # properly namespacing them. Each subscription has the following
196
+ # format:
197
+ # > projects/<gcp_project_id>/subscriptions/<gcp_sub_prefix>.<subscriber_class>.<topic>
198
+ #
199
+ config.gcp_sub_prefix = 'my-app'
200
+
201
+ #
202
+ # Specify the publicly accessible host for your application
203
+ #
204
+ # > E.g. in development, using the pub/sub local emulator
205
+ # config.processor_host = 'http://localhost:3000'
206
+ #
207
+ # > E.g. in development, using `config.mode = :production` and ngrok
208
+ # config.processor_host = 'https://111111.ngrok.io'
209
+ #
210
+ config.processor_host = 'https://app.mydomain.com'
211
+
212
+ #
213
+ # Specify the mode of operation:
214
+ # - :development => messages will be pushed to the local pub/sub emulator
215
+ # - :production => messages will be pushed to Google Cloud Pub/Sub. Requires a publicly accessible domain.
216
+ #
217
+ # Defaults to :development unless CLOUDENVOY_ENV or RAILS_ENV or RACK_ENV is set to something else.
218
+ #
219
+ # config.mode = Rails.env.production? || Rails.env.my_other_env? ? :production : :development
220
+
221
+ #
222
+ # Specify the logger to use
223
+ #
224
+ # Default with Rails: Rails.logger
225
+ # Default without Rails: Logger.new(STDOUT)
226
+ #
227
+ # config.logger = MyLogger.new(STDOUT)
228
+ end
229
+ ```
230
+
231
+ ### Creating topics and subscriptions
232
+
233
+ Topics and subscriptions can be created using the provided Rake tasks:
234
+ ```bash
235
+ # Setup publishers (topics) and subscribers (subscriptions) in one go
236
+ bundle exec rake cloudenvoy:setup
237
+
238
+ # Or set them up individually
239
+ bundle exec rake cloudenvoy:setup_publishers
240
+ bundle exec rake cloudenvoy:setup_subscribers
241
+ ```
242
+
243
+ For non-rails applications you can run the following in a console to setup your publishers and subscribers:
244
+ ```ruby
245
+ DummyPublisher.setup
246
+ DummySubscriber.setup
247
+ ```
248
+
249
+ ## Publishing messages
250
+
251
+ ### Sending messages
252
+
253
+ Cloudenvoy provides a helper method to publish arbitrary messages to any topic.
254
+ ```ruby
255
+ Cloudenvoy.publish('my-topic', { 'some' => 'payload' }, { 'optional' => 'message attribute' })
256
+ ```
257
+
258
+ This helper is useful for sending basic messages however it is not the preferred way of sending messages as you will quickly clutter your application with message formatting logic over time.
259
+
260
+ Cloudenvoy provides an object-oriented way of sending messages allowing developers to separate their core business logic from any kind of message formatting logic. These are called `Publishers`.
261
+
262
+ The example below shows you how to publish new users to a topic using Cloudenvoy publishers:
263
+ ```ruby
264
+ # app/publishers/user_publisher.rb
265
+
266
+ # The publisher is responsible for configuring and formatting
267
+ # the pub/sub message.
268
+ class UserPublisher
269
+ include Cloudenvoy::Publisher
270
+
271
+ cloudenvoy_options topic: 'system-users'
272
+
273
+ # Publishers must at least implement the `payload` method,
274
+ # which specifies how the message should be formatted.
275
+ def payload(user)
276
+ {
277
+ id: user.id,
278
+ name: user.name,
279
+ email: user.email
280
+ }
281
+ end
282
+ end
283
+ ```
284
+
285
+ Then in your user model you can do the following:
286
+ ```ruby
287
+ # app/users/user_publisher.rb
288
+
289
+ class User < ApplicationRecord
290
+ after_create :publish_user
291
+
292
+ private
293
+
294
+ # Publish users after they have been created
295
+ def publish_user
296
+ UserPublisher.publish(self)
297
+ end
298
+ end
299
+ ```
300
+
301
+ ### Publisher implementation layout
302
+
303
+ A full publisher implementation looks like this:
304
+ ```ruby
305
+ class MyPublisher
306
+ include Cloudenvoy::Publisher
307
+
308
+ # The topic option defines the default topic messages will be
309
+ # sent to. The publishing topic can be overriden on a per message
310
+ # basis. See the #topic method below.
311
+ cloudenvoy_options topic: 'my-topic'
312
+
313
+ # Evaluate the topic at runtime based on publishing arguments.
314
+ # Returning `nil` makes the publisher use the default topic
315
+ # defined via cloudenvoy_options.
316
+ #
317
+ # Note: runtime topics do not get created by the rake tasks. You
318
+ # must create them manually at this stage.
319
+ def topic(arg1, arg2)
320
+ arg1 == 'other' ? 'some-other-topic' : nil
321
+ end
322
+
323
+ # Attach pub/sub attributes to the message. Pub/sub attributes
324
+ # can be used for message filtering.
325
+ def metadata(arg1, arg2)
326
+ { reference: "#{arg1}_#{arg2}" }
327
+ end
328
+
329
+ # Publishers must at least implement the `payload` method,
330
+ # which specifies how arguments should be transformed into
331
+ # a message payload (Hash or String).
332
+ def payload(arg1, arg2)
333
+ {
334
+ foo: arg1,
335
+ bar: arg2
336
+ }
337
+ end
338
+
339
+ # This hook is invoked when the message fails to be formatted and published.
340
+ # If something wrong happens in the methods above, this hook will be triggered.
341
+ def on_error(error)
342
+ logger.error("Oops! Something wrong happened!")
343
+ end
344
+ end
345
+ ```
346
+
347
+ ## Receiving messages
348
+
349
+ After you have subscribed to a topic, Pub/Sub sends messages to your application via webhook on the `/cloudenvoy/receive` endpoint. Cloudenvoy then automatically dispatches the message to the right subscriber for processing.
350
+
351
+ Following up on the previous user publishing example, you might define the following subscriber in another Rails application:
352
+
353
+ ```ruby
354
+ # app/subscribers/user_subscriber.rb
355
+
356
+ class UserSubscriber
357
+ include Cloudenvoy::Subscriber
358
+
359
+ # Subscribers can subscribe to multiple topics
360
+ cloudenvoy_options topics: ['system-users']
361
+
362
+ # Create the user locally if it does not exist already
363
+ #
364
+ # A message has the following attributes:
365
+ # id: the pub/sub message id
366
+ # payload: the content of the message (String or Hash)
367
+ # metadata: the pub/sub message attributes
368
+ # sub_uri: the pub/sub subscription URI
369
+ # topic: the topic the message comes from
370
+ #
371
+ def process(message)
372
+ payload = message.payload
373
+
374
+ User.create_or_find_by(system_id: payload['id']) do |u|
375
+ u.first_name = payload['name']
376
+ u.email = payload['email']
377
+ end
378
+ end
379
+
380
+ # This hook will be invoked if the message processing fails
381
+ def on_error(error)
382
+ logger.error("The following error happened: #{error}")
383
+ end
384
+ end
385
+ ```
386
+
387
+ ## Logging
388
+ There are several options available to configure logging and logging context.
389
+
390
+ ### Configuring a logger
391
+ Cloudenvoy uses `Rails.logger` if Rails is available and falls back on a plain ruby logger `Logger.new(STDOUT)` if not.
392
+
393
+ It is also possible to configure your own logger. For example you can setup Cloudenvoy with [semantic_logger](http://rocketjob.github.io/semantic_logger) by doing the following in your initializer:
394
+ ```ruby
395
+ # config/initializers/cloudenvoy.rb
396
+
397
+ Cloudenvoy.configure do |config|
398
+ config.logger = SemanticLogger[Cloudenvoy]
399
+ end
400
+ ```
401
+
402
+ ### Logging context
403
+ Cloudenvoy provides publisher/subscriber contextual information to the `logger` methods.
404
+
405
+ For example:
406
+ ```ruby
407
+ # app/subscribers/dummy_subscriber.rb
408
+
409
+ class DummySubscriber
410
+ include Cloudenvoy::Subscriber
411
+
412
+ cloudenvoy_options topics: ['my-topic']
413
+
414
+ def process(message)
415
+ logger.info("Subscriber processed with #{message.inspect}. This is working!")
416
+ end
417
+ end
418
+ ```
419
+
420
+ Will generate the following log with context `{:id=>..., :metadata=>..., :topic=>...}`
421
+ ```log
422
+ [Cloudenvoy][DummySubscriber][1501678353930997] Subscriber processed with ###. This is working! -- {:id=>"1501678353930997", :metadata=>{"some"=>"meta"}, :topic=>"my-topic"}
423
+ ```
424
+
425
+ The way contextual information is displayed depends on the logger itself. For example with [semantic_logger](http://rocketjob.github.io/semantic_logger) contextual information might not appear in the log message but show up as payload data on the log entry itself (e.g. using the fluentd adapter).
426
+
427
+ Contextual information can be customised globally and locally using a log context_processor. By default the loggers are configured this way:
428
+ ```ruby
429
+ # Publishers
430
+ Cloudenvoy::PublisherLogger.log_context_processor = ->(publisher) { publisher.message&.to_h&.slice(:id, :metadata, :topic) || {} }
431
+
432
+ # Subscribers
433
+ Cloudenvoy::SubscriberLogger.log_context_processor = ->(subscriber) { subscriber.message.to_h.slice(:id, :metadata, :topic) }
434
+ ```
435
+
436
+ You can decide to add a global identifier for your publisher logs using the following:
437
+ ```ruby
438
+ # config/initializers/cloudenvoy.rb
439
+
440
+ Cloudenvoy::PublisherLogger.log_context_processor = lambda { |publisher|
441
+ publisher.message.to_h.slice(:id, :metadata, :topic).merge(app: 'my-app')
442
+ }
443
+ ```
444
+
445
+ You could also decide to log all available context - including the message payload - for specific subscribers only:
446
+ ```ruby
447
+ # app/subscribers/full_context_subscriber.rb
448
+
449
+ class FullContextSubscriber
450
+ include Cloudenvoy::Subscriber
451
+
452
+ cloudenvoy_options topics: ['my-topic'], log_context_processor: ->(s) { s.message.to_h }
453
+
454
+ def process(message)
455
+ logger.info("This log entry will have full context!")
456
+ end
457
+ end
458
+ ```
459
+
460
+ See the [Cloudenvoy::Publisher](lib/cloudenvoy/publisher.rb), [Cloudenvoy::Subscriber](lib/cloudenvoy/subscriber.rb) and [Cloudenvoy::Message](lib/cloudenvoy/message.rb) for more information on attributes available to be logged in your `log_context_processor` proc.
461
+
462
+ ## Error Handling
463
+
464
+ Message failures will return an HTTP error to Pub/Sub and trigger a retry at a later time. By default Pub/Sub will retry sending the message until the acknowledgment deadline expires. A number of retries can be explicitly configured by setting up a dead-letter queue.
465
+
466
+ ### HTTP Error codes
467
+
468
+ When Cloudenvoy fails to process a message it returns the following HTTP error code to Pub/Sub, based on the actual reason:
24
469
 
25
- TODO: Write usage instructions here
470
+ | Code | Description |
471
+ |------|-------------|
472
+ | 204 | The message was processed successfully |
473
+ | 404 | The message subscriber does not exist. |
474
+ | 422 | An error occured during the processing of the message (`process` method) |
475
+
476
+ ### Error callbacks
477
+
478
+ Publishers and subscribers can implement the `on_error(error)` callback to do things when a message fails to be published or received:
479
+
480
+ E.g.
481
+ ```ruby
482
+ # app/publisher/handle_error_publisher.rb
483
+
484
+ class HandleErrorPublisher
485
+ include Cloudenvoy::Publisher
486
+
487
+ cloudenvoy_options topic: 'my-topic'
488
+
489
+ def payload(arg)
490
+ raise(ArgumentError)
491
+ end
492
+
493
+ # The runtime error is passed as an argument.
494
+ def on_error(error)
495
+ logger.error("The following error occured: #{error}")
496
+ end
497
+ end
498
+ ```
499
+
500
+ ## Testing
501
+ Cloudenvoy provides several options to test your publishers and subscribers.
502
+
503
+ ### Test helper setup
504
+ Require `cloudenvoy/testing` in your `rails_helper.rb` (Rspec Rails) or `spec_helper.rb` (Rspec) or test unit helper file then enable one of the two modes:
505
+
506
+ ```ruby
507
+ require 'cloudenvoy/testing'
508
+
509
+ # Mode 1 (default): Push messages to GCP Pub/Sub (env != development)
510
+ Cloudenvoy::Testing.enable!
511
+
512
+ # Mode 2: Push message to in-memory queues. You will be responsible for clearing the
513
+ # topic queues using `Cloudenvoy::Testing.clear_all` or `Cloudenvoy::Testing.clear('my-topic')`
514
+ Cloudenvoy::Testing.fake!
515
+ ```
516
+
517
+ You can query the current testing mode with:
518
+ ```ruby
519
+ Cloudenvoy::Testing.enabled?
520
+ Cloudenvoy::Testing.fake?
521
+ ```
522
+
523
+ Each testing mode accepts a block argument to temporarily switch to it:
524
+ ```ruby
525
+ # Enable fake mode for all tests
526
+ Cloudenvoy::Testing.fake!
527
+
528
+ # Enable real mode temporarily for a given test
529
+ Cloudenvoy.enable! do
530
+ MyPublisher.publish(1,2)
531
+ end
532
+ ```
533
+
534
+ Note that extension middlewares - if any has been registered - run in test mode. You can disable middlewares in your tests by adding the following to your test helper:
535
+ ```ruby
536
+ # Remove all middlewares
537
+ Cloudenvoy.configure do |c|
538
+ c.publisher_middleware.clear
539
+ c.subscriber_middleware.clear
540
+ end
541
+
542
+ # Remove all specific middlewares
543
+ Cloudenvoy.configure do |c|
544
+ c.publisher_middleware.remove(MyMiddleware::Publisher)
545
+ c.subscriber_middleware.remove(MyMiddleware::Subscriber)
546
+ end
547
+ ```
548
+
549
+ ### In-memory queues
550
+ The `fake!` modes uses in-memory queues for topics, which can be queried and controlled using the following methods:
551
+
552
+ ```ruby
553
+ # Clear all messages across all topics
554
+ Cloudenvoy::Testing.clear_all
555
+
556
+ # Remove all messages in a given topic
557
+ Cloudenvoy::Testing.clear('my-top')
558
+
559
+ # Get all messages for a given topic
560
+ Cloudenvoy::Testing.queue('my-top')
561
+ ```
562
+
563
+ ### Unit tests
564
+ Below are examples of rspec tests. It is assumed that `Cloudenvoy::Testing.fake!` has been set in the test helper.
565
+
566
+ **Example 1**: Testing publishers
567
+ ```ruby
568
+ describe 'message publishing'
569
+ subject(:publish_message) { MyPublisher.publish(1,2) }
570
+
571
+ let(:queue) { Cloudenvoy::Testing.queue('my-topic') }
572
+
573
+ it { expect { publish_message }.to change(queue, :size).by(1) }
574
+ it { is_expected.to have_attributes(payload: { 'foo' => 'bar' }) }
575
+ end
576
+ ```
577
+
578
+ **Example 2**: Testing subscribers
579
+ ```ruby
580
+ describe 'message processing'
581
+ subject { VerifyDataViaApiSubscriber.new(message: message).execute } }
582
+
583
+ let(:message) { Cloudenvoy::Message.new(payload: { 'some' => 'payload' }) }
584
+
585
+ before { expect(MyApi).to receive(:fetch).and_return([]) }
586
+ it { is_expected.to be_truthy }
587
+ end
588
+ ```
26
589
 
27
590
  ## Development
28
591
 
@@ -32,8 +595,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
595
 
33
596
  ## Contributing
34
597
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/alachaum/cloudenvoy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/alachaum/cloudenvoy/blob/master/CODE_OF_CONDUCT.md).
36
-
598
+ Bug reports and pull requests are welcome on GitHub at https://github.com/keypup-io/cloudenvoy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/keypup-io/cloudenvoy/blob/master/CODE_OF_CONDUCT.md).
37
599
 
38
600
  ## License
39
601
 
@@ -41,4 +603,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
41
603
 
42
604
  ## Code of Conduct
43
605
 
44
- Everyone interacting in the Cloudenvoy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alachaum/cloudenvoy/blob/master/CODE_OF_CONDUCT.md).
606
+ Everyone interacting in the Cloudenvoy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/keypup-io/cloudenvoy/blob/master/CODE_OF_CONDUCT.md).