eventhub-processor2 1.26.1 → 1.27.0

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: d95676a331273b8afbe6f83ed5349e515ff7631d3217df6338b79f2e6e125f4f
4
- data.tar.gz: 18b6632417f91ff8795d9e2b755dea4cd4c0f0eddfec10afa9a375c9ba717a13
3
+ metadata.gz: 71103595a699bfa32e3c771073f3b870a5e10bba02a9191585e5bd2369a4fb8b
4
+ data.tar.gz: 82045e01a8f8c136ba5413c6a1153093fd441db66704352d58034f371602dbba
5
5
  SHA512:
6
- metadata.gz: db9e521ff4185fd17012c8beaf62e11d440469116c77a02b246c4b675e4abab3d3f863985f4a55c99908cffeec2f500298f2bbffb803f4fabcf41934ca1774b3
7
- data.tar.gz: d6686f41f63a610a7a66eaf3f476e8e5a0d0de3a8e387fc70a972969b95690170c82e8d5f1ae06d05afc38a8dc69c4ff1c0dffe0f1604cb299458d954c958600
6
+ metadata.gz: 99f35f36aacf9e25df8c7ecafc8c26dfd41f4dac7e3d486ae904aa8555b6af7b2c60688aab5b68c1cc1285117212c9b7b6e10ef1ce6b38489860bdd21094188e
7
+ data.tar.gz: a6011ab9c3345016e4f3b3c35d00685ea64635683f61ff38e7d6a555a8b1edd1993ce8e6be38460be45251d1e371b0810277a9df78d93891d155239aefe5f040
@@ -42,6 +42,8 @@ jobs:
42
42
  steps:
43
43
  - name: Checkout current code
44
44
  uses: actions/checkout@v6
45
+ with:
46
+ ref: main
45
47
 
46
48
  - name: Set up Ruby
47
49
  uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog of EventHub::Processor2
2
2
 
3
+ # 1.27.0 / 2026-04-01
4
+
5
+ * Adapt to Bunny 3.0 publisher confirms: use `confirm_select(tracking: true)` for automatic backpressure, remove manual `wait_for_confirms` calls
6
+ * Remove `:configuration` from default HTTP resources to prevent automatic exposure of sensitive config on upgrade (opt-in via `http_resources` method)
7
+ * Improve configuration page: compact rendering for nested hashes and arrays, show `(not set)` for empty values, add client-side filter with reset button, visual distinction for top-level sections
8
+ * Fix deprecation logic to only warn when `heartbeat.*` and `http.*` values actually differ; remove defaults from deprecated `heartbeat` config block
9
+ * Fix example processors: add unique HTTP ports, create missing `data/` directory in publisher
10
+ * Fix flaky sleeper specs by widening timing tolerance and using idiomatic RSpec `be_within` matcher
11
+
12
+ # 1.26.2 / 2026-03-25
13
+
14
+ * Change default `http.bind_address` from `localhost` to `0.0.0.0` for container compatibility (ECS, Docker, K8s)
15
+ * Add deprecation warning when legacy `heartbeat` configuration block is used
16
+
3
17
  # 1.26.1 / 2026-03-20
4
18
 
5
19
  * Show only processor name in browser tab title
data/README.md CHANGED
@@ -204,7 +204,7 @@ If --config option is not provided processor tries to load config/{class_name}.j
204
204
  "verify_peer": false,
205
205
  "show_bunny_logs": false,
206
206
  "http": {
207
- "bind_address": "localhost",
207
+ "bind_address": "0.0.0.0",
208
208
  "port": 8080,
209
209
  "base_path": "/svc/{class_name}"
210
210
  }
@@ -307,7 +307,7 @@ Processor2 exposes HTTP resources for health checks and monitoring. All resource
307
307
  {
308
308
  "server": {
309
309
  "http": {
310
- "bind_address": "localhost",
310
+ "bind_address": "0.0.0.0",
311
311
  "port": 8080,
312
312
  "base_path": "/svc/{class_name}"
313
313
  }
@@ -315,17 +315,19 @@ Processor2 exposes HTTP resources for health checks and monitoring. All resource
315
315
  }
316
316
  ```
317
317
 
318
+ The default `bind_address` is `0.0.0.0` (all interfaces), which is required for containerized deployments (ECS, Docker, K8s) where health checks come from outside the container.
319
+
318
320
  Resources are mounted under the `base_path`:
319
321
  - `{base_path}/heartbeat` - Health check
320
322
  - `{base_path}/version` - Version info as JSON
321
323
  - `{base_path}/docs` - README documentation as HTML
322
- - `{base_path}/docs/configuration` - Configuration as HTML table
324
+ - `{base_path}/docs/configuration` - Configuration as HTML table (opt-in, see [Enabling Resources](#enabling-resources))
323
325
  - `{base_path}/docs/changelog` - CHANGELOG as HTML
324
326
  - `{base_path}/assets/*` - Static assets (CSS, images)
325
327
 
326
328
  Accessing `{base_path}` or `{base_path}/` redirects to `{base_path}/docs`.
327
329
 
328
- **Backward Compatibility:** If you have existing configuration using the old `heartbeat` block with `bind_address`, `port`, and `path`, it will continue to work. The new `http` configuration takes precedence when both are present.
330
+ **Backward Compatibility:** If you have existing configuration using the old `heartbeat` block with `bind_address`, `port`, and `path`, it will continue to work but will emit a deprecation warning. Please migrate to the `http` configuration block. The `heartbeat` block will be removed in a future major version.
329
331
 
330
332
  ### Heartbeat
331
333
 
@@ -467,7 +469,7 @@ GET {base_path}/docs/configuration
467
469
 
468
470
  **Response:** `200 OK` with HTML page
469
471
 
470
- By default, the following keys are redacted: `password`, `secret`, `token`, `api_key`, `credential`. You can customize the list by defining a `sensitive_keys` method in your processor:
472
+ By default, the following keys are redacted: `password`, `secret`, `token`, `api_key`, `credential`, `username`, `user`, `login`. You can customize the list by defining a `sensitive_keys` method in your processor:
471
473
 
472
474
  ```ruby
473
475
  # Override the entire list
@@ -495,28 +497,22 @@ class MyProcessor < EventHub::Processor2
495
497
  end
496
498
  ```
497
499
 
498
- ### Disabling Resources
500
+ ### Enabling Resources
499
501
 
500
- By default, all HTTP resources are enabled. You can control which resources are available by defining an `http_resources` method in your processor. The navbar adapts automatically.
502
+ By default, the following HTTP resources are enabled: `:heartbeat`, `:version`, `:docs`, and `:changelog`. The `:configuration` resource is **disabled by default** because it displays server configuration which may contain sensitive values. Although passwords, tokens, and keys are automatically redacted, we prefer a secure-by-default approach where processors must explicitly opt in to exposing configuration.
501
503
 
502
- ```ruby
503
- class MyProcessor < EventHub::Processor2
504
- def http_resources
505
- [:heartbeat, :version, :docs, :changelog, :configuration] # default: all enabled
506
- end
507
- end
508
- ```
509
-
510
- To disable the configuration page for example:
504
+ To enable the configuration page, define an `http_resources` method in your processor:
511
505
 
512
506
  ```ruby
513
507
  class MyProcessor < EventHub::Processor2
514
508
  def http_resources
515
- [:heartbeat, :version, :docs, :changelog]
509
+ [:heartbeat, :version, :docs, :changelog, :configuration]
516
510
  end
517
511
  end
518
512
  ```
519
513
 
514
+ You can also use `http_resources` to disable any of the default resources. The navbar adapts automatically.
515
+
520
516
  ### Customizing Footer
521
517
 
522
518
  The documentation pages display company name, version, and environment in the footer. Company name defaults to "Novartis" but can be customized by defining a `company_name` method in your processor:
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.add_dependency "celluloid", "~> 0.18"
26
26
  spec.add_dependency "webrick", "~> 1.8"
27
- spec.add_dependency "bunny", "~> 2.23"
27
+ spec.add_dependency "bunny", "~> 3.0"
28
28
  spec.add_dependency "eventhub-components", "~> 0.4"
29
29
  spec.add_dependency "base64", "~> 0.3.0"
30
30
  spec.add_dependency "logger", "~> 1.6"
@@ -0,0 +1,15 @@
1
+ {
2
+ "development": {
3
+ "server": {
4
+ "user": "guest",
5
+ "password": "guest",
6
+ "host": "localhost",
7
+ "vhost": "event_hub",
8
+ "port": 5672,
9
+ "tls": false,
10
+ "http": {
11
+ "port": 8083
12
+ }
13
+ }
14
+ }
15
+ }
@@ -6,7 +6,10 @@
6
6
  "host": "localhost",
7
7
  "vhost": "event_hub",
8
8
  "port": 5672,
9
- "tls": false
9
+ "tls": false,
10
+ "http": {
11
+ "port": 8081
12
+ }
10
13
  },
11
14
  "processor": {
12
15
  "listener_queues": [
@@ -6,7 +6,10 @@
6
6
  "host": "localhost",
7
7
  "vhost": "event_hub",
8
8
  "port": 5672,
9
- "tls": false
9
+ "tls": false,
10
+ "http": {
11
+ "port": 8082
12
+ }
10
13
  },
11
14
  "processor": {
12
15
  "listener_queues": [
data/example/example.rb CHANGED
@@ -6,6 +6,10 @@ module EventHub
6
6
  "1.0.0" # define your version
7
7
  end
8
8
 
9
+ def http_resources
10
+ [:heartbeat, :version, :docs, :changelog, :configuration]
11
+ end
12
+
9
13
  def handle_message(message, args = {})
10
14
  # deal with your parsed EventHub message
11
15
  # message.class => EventHub::Message
data/example/publisher.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "bunny"
2
2
  require "celluloid"
3
+ require "fileutils"
3
4
  require "json"
4
5
  require "securerandom"
5
6
  require "eventhub/components"
@@ -38,6 +39,7 @@ module Publisher
38
39
  @files_sent = 0
39
40
 
40
41
  @filename = "data/store.json"
42
+ FileUtils.mkdir_p(File.dirname(@filename))
41
43
  if File.exist?(@filename)
42
44
  cleanup
43
45
  else
@@ -119,7 +121,7 @@ module Publisher
119
121
  logger: Logger.new(File::NULL))
120
122
  @connection.start
121
123
  @channel = @connection.create_channel
122
- @channel.confirm_select
124
+ @channel.confirm_select(tracking: true)
123
125
  @exchange = @channel.direct("example.outbound", durable: true)
124
126
  end
125
127
 
@@ -135,13 +137,8 @@ module Publisher
135
137
  Publisher.logger.info("[#{id}] - Message/File created")
136
138
 
137
139
  @exchange.publish(data, persistent: true)
138
- success = @channel.wait_for_confirms
139
- if success
140
- Celluloid::Actor[:transaction_store]&.stop(id)
141
- Publisher&.logger&.info("[#{id}] - Message sent")
142
- else
143
- Publisher&.logger&.error("[#{id}] - Published message not confirmed")
144
- end
140
+ Celluloid::Actor[:transaction_store]&.stop(id)
141
+ Publisher&.logger&.info("[#{id}] - Message sent")
145
142
  end
146
143
  end
147
144
 
data/example/receiver.rb CHANGED
@@ -10,6 +10,10 @@ module EventHub
10
10
  "Example Company"
11
11
  end
12
12
 
13
+ def http_resources
14
+ [:heartbeat, :version, :docs, :changelog, :configuration]
15
+ end
16
+
13
17
  # Custom README served via method instead of file
14
18
  def readme_as_html
15
19
  <<~HTML
data/example/router.rb CHANGED
@@ -7,6 +7,10 @@ module EventHub
7
7
  "1.0.0"
8
8
  end
9
9
 
10
+ def http_resources
11
+ [:heartbeat, :version, :docs, :changelog, :configuration]
12
+ end
13
+
10
14
  def handle_message(message, args = {})
11
15
  id = message.body["id"]
12
16
  EventHub.logger.info("Received: [#{id}]")
@@ -38,15 +38,9 @@ module EventHub
38
38
  connection = create_bunny_connection
39
39
  connection.start
40
40
  channel = connection.create_channel
41
- channel.confirm_select
41
+ channel.confirm_select(tracking: true)
42
42
  exchange = channel.direct(EventHub::EH_X_INBOUND, durable: true)
43
43
  exchange.publish(message, persistent: true)
44
- success = channel.wait_for_confirms
45
-
46
- unless success
47
- raise "Published heartbeat message has " \
48
- "not been confirmed by the server"
49
- end
50
44
  ensure
51
45
  connection&.close
52
46
  end
@@ -10,7 +10,7 @@ module EventHub
10
10
  include Helper
11
11
 
12
12
  DEFAULT_VERSION = "?.?.?"
13
- DEFAULT_HTTP_RESOURCES = [:heartbeat, :version, :docs, :changelog, :configuration].freeze
13
+ DEFAULT_HTTP_RESOURCES = [:heartbeat, :version, :docs, :changelog].freeze
14
14
  CONTENT_TYPES = {
15
15
  ".css" => "text/css",
16
16
  ".svg" => "image/svg+xml",
@@ -183,9 +183,17 @@ module EventHub
183
183
  private
184
184
 
185
185
  def http_config(key)
186
- # Try new http config first, fall back to deprecated heartbeat config
187
- EventHub::Configuration.server.dig(:http, key) ||
188
- EventHub::Configuration.server.dig(:heartbeat, key)
186
+ # Prefer deprecated heartbeat config for backward compatibility,
187
+ # fall back to http config. Only warn when values actually differ.
188
+ heartbeat_value = EventHub::Configuration.server.dig(:heartbeat, key)
189
+ http_value = EventHub::Configuration.server.dig(:http, key)
190
+
191
+ if heartbeat_value && http_value && heartbeat_value != http_value
192
+ EventHub.logger.warn("[DEPRECATION] heartbeat.#{key} is deprecated. Please use http.#{key} instead.")
193
+ return heartbeat_value
194
+ end
195
+
196
+ http_value || heartbeat_value
189
197
  end
190
198
 
191
199
  def resource_enabled?(name)
@@ -25,7 +25,7 @@ module EventHub
25
25
  exchange_name = args[:exchange_name] || EH_X_INBOUND
26
26
 
27
27
  channel = @connection.create_channel
28
- channel.confirm_select
28
+ channel.confirm_select(tracking: true)
29
29
  exchange = channel.direct(exchange_name, durable: true)
30
30
 
31
31
  publish_options = {persistent: true}
@@ -33,12 +33,6 @@ module EventHub
33
33
  publish_options[:correlation_id] = correlation_id if correlation_id
34
34
 
35
35
  exchange.publish(message, publish_options)
36
- success = channel.wait_for_confirms
37
-
38
- unless success
39
- raise "Published message from Listener actor " \
40
- "has not been confirmed by the server"
41
- end
42
36
  ensure
43
37
  channel&.close
44
38
  end
@@ -94,6 +94,54 @@ body {
94
94
  width: auto;
95
95
  }
96
96
 
97
+ /* Config filter */
98
+ .config-filter {
99
+ margin-bottom: 1rem;
100
+ }
101
+
102
+ .config-filter-row {
103
+ display: flex;
104
+ align-items: center;
105
+ max-width: 400px;
106
+ }
107
+
108
+ .config-filter .input {
109
+ flex: 1;
110
+ padding: 0.5rem 0.75rem;
111
+ font-size: 0.95rem;
112
+ border: 1px solid #dbdbdb;
113
+ border-radius: 4px 0 0 4px;
114
+ }
115
+
116
+ .config-filter .input:focus {
117
+ border-color: hsl(212, 55%, 48%);
118
+ outline: none;
119
+ box-shadow: 0 0 0 2px hsla(212, 55%, 48%, 0.2);
120
+ }
121
+
122
+ .config-filter-reset {
123
+ padding: 0 0.75rem;
124
+ font-size: 1.1rem;
125
+ border: 1px solid #dbdbdb;
126
+ border-left: none;
127
+ border-radius: 0 4px 4px 0;
128
+ background: #f5f5f5;
129
+ color: #7a7a7a;
130
+ cursor: pointer;
131
+ align-self: stretch;
132
+ }
133
+
134
+ .config-filter-reset:hover {
135
+ background: #e8e8e8;
136
+ color: #363636;
137
+ }
138
+
139
+ .config-filter-count {
140
+ margin-top: 0.25rem;
141
+ font-size: 0.85rem;
142
+ color: #7a7a7a;
143
+ }
144
+
97
145
  /* Config table */
98
146
  .config-table thead th {
99
147
  background-color: #1a1a1a !important;
@@ -107,6 +155,10 @@ body {
107
155
  font-weight: 600;
108
156
  }
109
157
 
158
+ .config-table .is-section-top td {
159
+ background-color: #d5d5d5;
160
+ }
161
+
110
162
  .config-table tbody tr:not(.is-section):hover td {
111
163
  background-color: hsl(212, 55%, 93%) !important;
112
164
  }
@@ -116,11 +168,37 @@ body {
116
168
  padding-left: 2rem;
117
169
  }
118
170
 
119
- .config-table .redacted {
171
+ .config-table .redacted,
172
+ .config-table .not-set {
120
173
  color: #b5b5b5;
121
174
  font-style: italic;
122
175
  }
123
176
 
177
+ .config-array {
178
+ list-style: none;
179
+ margin: 0;
180
+ padding: 0;
181
+ }
182
+
183
+ .config-array > li {
184
+ padding: 0.25rem 0;
185
+ }
186
+
187
+ .config-array > li + li {
188
+ border-top: 1px solid #e8e8e8;
189
+ }
190
+
191
+ .config-subtable {
192
+ margin: 0 !important;
193
+ font-size: 0.9rem;
194
+ }
195
+
196
+ .config-subtable td:first-child {
197
+ font-weight: 600;
198
+ white-space: nowrap;
199
+ width: 1%;
200
+ }
201
+
124
202
  /* Footer */
125
203
  .footer {
126
204
  padding: 1.5rem;
@@ -138,7 +138,7 @@ module EventHub
138
138
  verify_peer: false,
139
139
  show_bunny_logs: false,
140
140
  http: {
141
- bind_address: "localhost",
141
+ bind_address: "0.0.0.0",
142
142
  port: 8080,
143
143
  base_path: "/svc/#{@name}",
144
144
  docs: {
@@ -147,11 +147,7 @@ module EventHub
147
147
  }
148
148
  },
149
149
  # deprecated: use http instead (kept for backward compatibility)
150
- heartbeat: {
151
- bind_address: "localhost",
152
- port: 8080,
153
- path: "/svc/#{@name}/heartbeat"
154
- }
150
+ heartbeat: {}
155
151
  },
156
152
  processor: {
157
153
  heartbeat_cycle_in_s: 300,
@@ -10,7 +10,7 @@ module EventHub
10
10
  DEFAULT_CHANGELOG_LOCATIONS = ["CHANGELOG.md", "doc/CHANGELOG.md"].freeze
11
11
  DEFAULT_COMPANY_NAME = "Novartis"
12
12
 
13
- DEFAULT_HTTP_RESOURCES = [:heartbeat, :version, :docs, :changelog, :configuration].freeze
13
+ DEFAULT_HTTP_RESOURCES = [:heartbeat, :version, :docs, :changelog].freeze
14
14
 
15
15
  def initialize(processor:, base_path:)
16
16
  @processor = processor
@@ -91,19 +91,101 @@ module EventHub
91
91
  "<p>Active configuration for the <strong>#{ERB::Util.html_escape(EventHub::Configuration.environment)}</strong> environment. " \
92
92
  "Sensitive values such as passwords, tokens, and keys are automatically redacted.</p>"
93
93
 
94
- intro + config_to_html_table(config)
94
+ filter = '<div class="config-filter">' \
95
+ '<div class="config-filter-row">' \
96
+ '<input type="text" id="config-filter-input" class="input" placeholder="Filter configuration keys..." autocomplete="off">' \
97
+ '<button type="button" id="config-filter-reset" class="config-filter-reset" title="Reset filter">&times;</button>' \
98
+ "</div>" \
99
+ '<p id="config-filter-count" class="config-filter-count"></p>' \
100
+ "</div>"
101
+
102
+ script = <<~JS
103
+ <script>
104
+ (function() {
105
+ var input = document.getElementById('config-filter-input');
106
+ var reset = document.getElementById('config-filter-reset');
107
+ var count = document.getElementById('config-filter-count');
108
+ var table = document.querySelector('.config-table');
109
+ if (!input || !table) return;
110
+
111
+ input.addEventListener('input', function() {
112
+ var term = this.value.toLowerCase();
113
+ var rows = table.querySelectorAll('tbody tr');
114
+ var visible = 0;
115
+ var total = 0;
116
+
117
+ // Filter individual rows by content (includes sub-tables)
118
+ rows.forEach(function(row) {
119
+ if (row.classList.contains('is-section')) {
120
+ row.style.display = '';
121
+ return;
122
+ }
123
+ total++;
124
+ if (!term || row.textContent.toLowerCase().indexOf(term) !== -1) {
125
+ row.style.display = '';
126
+ visible++;
127
+ } else {
128
+ row.style.display = 'none';
129
+ }
130
+ });
131
+
132
+ // Hide section headers with no visible rows after them
133
+ var sections = table.querySelectorAll('tbody tr.is-section');
134
+ sections.forEach(function(section) {
135
+ var next = section.nextElementSibling;
136
+ var hasVisible = false;
137
+ while (next && !next.classList.contains('is-section')) {
138
+ if (next.style.display !== 'none') hasVisible = true;
139
+ next = next.nextElementSibling;
140
+ }
141
+ section.style.display = hasVisible ? '' : 'none';
142
+ });
143
+
144
+ if (term) {
145
+ count.textContent = visible + ' of ' + total + ' entries';
146
+ } else {
147
+ count.textContent = '';
148
+ }
149
+ });
150
+ reset.addEventListener('click', function() {
151
+ input.value = '';
152
+ input.dispatchEvent(new Event('input'));
153
+ input.focus();
154
+ });
155
+ })();
156
+ </script>
157
+ JS
158
+
159
+ intro + filter + config_to_html_table(config) + script
95
160
  end
96
161
 
97
- def config_to_html_table(hash, depth = 0)
162
+ def config_to_html_table(hash, depth = 0, prefix = "")
98
163
  rows = hash.map do |key, value|
99
- if value.is_a?(Hash)
100
- "<tr class=\"is-section\"><td colspan=\"2\"><strong>#{ERB::Util.html_escape(key)}</strong></td></tr>\n" \
101
- "#{config_to_html_table(value, depth + 1)}"
164
+ full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
165
+ if depth == 0 && value.is_a?(Hash) && !value.empty?
166
+ "<tr class=\"is-section is-section-top\"><td colspan=\"2\"><strong>#{ERB::Util.html_escape(full_key)}</strong></td></tr>\n" \
167
+ "#{config_to_html_table(value, 1, full_key)}"
168
+ elsif value.is_a?(Hash) && value.empty?
169
+ "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td><span class=\"not-set\">(empty)</span></td></tr>"
170
+ elsif value.is_a?(Hash) && value.values.all? { |v| v.is_a?(Hash) && v.empty? }
171
+ items = value.keys.map { |k| "<li>#{ERB::Util.html_escape(k)}</li>" }.join("\n")
172
+ "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td><ul class=\"config-array\">#{items}</ul></td></tr>"
173
+ elsif value.is_a?(Hash) && compact_hash?(value)
174
+ "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td>#{format_nested_value(value)}</td></tr>"
175
+ elsif value.is_a?(Hash)
176
+ "<tr class=\"is-section\"><td colspan=\"2\"><strong>#{ERB::Util.html_escape(full_key)}</strong></td></tr>\n" \
177
+ "#{config_to_html_table(value, depth + 1, full_key)}"
102
178
  elsif value.is_a?(Array)
103
- format_array_rows(key, value, depth)
179
+ format_array_rows(full_key, key, value, depth)
104
180
  else
105
- display_value = sensitive_key?(key) ? "<span class=\"redacted\">[REDACTED]</span>" : ERB::Util.html_escape(value.to_s)
106
- "<tr><td class=\"config-key\">#{ERB::Util.html_escape(key)}</td><td>#{display_value}</td></tr>"
181
+ display_value = if sensitive_key?(key)
182
+ "<span class=\"redacted\">***</span>"
183
+ elsif value.nil? || value.to_s.strip.empty?
184
+ "<span class=\"not-set\">(not set)</span>"
185
+ else
186
+ ERB::Util.html_escape(value.to_s)
187
+ end
188
+ "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td>#{display_value}</td></tr>"
107
189
  end
108
190
  end.join("\n")
109
191
 
@@ -114,24 +196,59 @@ module EventHub
114
196
  end
115
197
  end
116
198
 
117
- def format_array_rows(key, array, depth)
118
- if array.any? { |item| item.is_a?(Hash) }
119
- array.each_with_index.map do |item, index|
120
- if item.is_a?(Hash)
121
- "<tr class=\"is-section\"><td colspan=\"2\"><strong>#{ERB::Util.html_escape(key)}[#{index}]</strong></td></tr>\n" \
122
- "#{config_to_html_table(item, depth + 1)}"
123
- else
124
- display_value = sensitive_key?(key) ? "<span class=\"redacted\">[REDACTED]</span>" : ERB::Util.html_escape(item.to_s)
125
- "<tr><td class=\"config-key\">#{ERB::Util.html_escape(key)}[#{index}]</td><td>#{display_value}</td></tr>"
126
- end
127
- end.join("\n")
199
+ def format_array_rows(full_key, key, array, _depth)
200
+ return "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td><span class=\"not-set\">(empty)</span></td></tr>" if array.empty?
201
+
202
+ if sensitive_key?(key)
203
+ return "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td><span class=\"redacted\">***</span></td></tr>"
204
+ end
205
+
206
+ inner = array.map { |item| format_array_item(item) }.join("\n")
207
+ "<tr><td class=\"config-key\">#{ERB::Util.html_escape(full_key)}</td><td><ul class=\"config-array\">#{inner}</ul></td></tr>"
208
+ end
209
+
210
+ def format_array_item(item)
211
+ if item.is_a?(Hash)
212
+ rows = item.map do |k, v|
213
+ value = format_nested_value(v)
214
+ "<tr><td>#{ERB::Util.html_escape(k)}</td><td>#{value}</td></tr>"
215
+ end.join
216
+ "<li><table class=\"table is-bordered is-narrow config-subtable\">#{rows}</table></li>"
217
+ elsif item.is_a?(Array)
218
+ inner = item.map { |i| format_array_item(i) }.join("\n")
219
+ "<li><ul class=\"config-array\">#{inner}</ul></li>"
128
220
  else
129
- display_value = sensitive_key?(key) ? "<span class=\"redacted\">[REDACTED]</span>" : ERB::Util.html_escape(array.join(", "))
130
- "<tr><td class=\"config-key\">#{ERB::Util.html_escape(key)}</td><td>#{display_value}</td></tr>"
221
+ "<li>#{ERB::Util.html_escape(item.to_s)}</li>"
222
+ end
223
+ end
224
+
225
+ def format_nested_value(value)
226
+ if value.is_a?(Hash)
227
+ rows = value.map do |k, v|
228
+ "<tr><td>#{ERB::Util.html_escape(k)}</td><td>#{format_nested_value(v)}</td></tr>"
229
+ end.join
230
+ "<table class=\"table is-bordered is-narrow config-subtable\">#{rows}</table>"
231
+ elsif value.is_a?(Array)
232
+ items = value.map { |i| format_array_item(i) }.join("\n")
233
+ "<ul class=\"config-array\">#{items}</ul>"
234
+ elsif value.nil? || value.to_s.strip.empty?
235
+ "<span class=\"not-set\">(not set)</span>"
236
+ else
237
+ ERB::Util.html_escape(value.to_s)
238
+ end
239
+ end
240
+
241
+ def compact_hash?(hash)
242
+ hash.values.all? do |v|
243
+ if v.is_a?(Hash)
244
+ compact_hash?(v)
245
+ else
246
+ !v.is_a?(Array)
247
+ end
131
248
  end
132
249
  end
133
250
 
134
- DEFAULT_SENSITIVE_KEYS = %w[password secret token api_key credential].freeze
251
+ DEFAULT_SENSITIVE_KEYS = %w[password secret token api_key credential username user login].freeze
135
252
 
136
253
  def sensitive_key?(key)
137
254
  keys = if @processor&.class&.method_defined?(:sensitive_keys)
@@ -1,3 +1,3 @@
1
1
  module EventHub
2
- VERSION = "1.26.1".freeze
2
+ VERSION = "1.27.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventhub-processor2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.1
4
+ version: 1.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steiner, Thomas
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.23'
46
+ version: '3.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.23'
53
+ version: '3.0'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: eventhub-components
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +192,7 @@ files:
192
192
  - eventhub-processor2.gemspec
193
193
  - example/CHANGELOG.md
194
194
  - example/README.md
195
+ - example/config/example.json
195
196
  - example/config/receiver.json
196
197
  - example/config/router.json
197
198
  - example/crasher.rb