eventhub-processor2 1.26.2 → 1.27.1

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: 3195a1503417b8ac4ce8dc345ad7263ceacde2d473229ccbc1ad5b1c18e3a2e4
4
- data.tar.gz: cf9b94c789e85c2b05fc3e9eea1c4f4ef4773b6f29fec0bdbf58256e73fbe280
3
+ metadata.gz: 902f7f788754624f1f2bb0aaf604cbb40950c0c214e58d0a340d6bd9d2f847a9
4
+ data.tar.gz: '092db4a1078ccc754bae694153c20a87e26218a731d88e1b37863d93cb900a4f'
5
5
  SHA512:
6
- metadata.gz: 9d3236c5f6db00ff7baf430811d34c1d2eb10be68a9cc7ed69c3463dc637528a6fd9d2b9433c3b94dd36c014bb2413b07366bbd9ebfce364527921a18a5db9b1
7
- data.tar.gz: c40045da1746f9aea76bb658e8a309921e80becdae9979d8adaa7989d018f96564870145ac72a9a8af38b20a7dce3c661b13b9a53ca1352de6af3cc7ea2828af
6
+ metadata.gz: 8d6993b13c319ad59d1b2a65212cc7c402044117d530d35f2387ca20db038d4cc6ab44b8a84e4a16dd99db3693c2f057203d81e61d6a4c51240068de08dad1c2
7
+ data.tar.gz: 1474a5bddd07a229101a2fec20bafb22252437d33f51c6c3ffdf193ba206f2125ff965890d2d450121a7994fa7c879dc2ccc7f721b2a1ce5c98d383f641c4e37
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog of EventHub::Processor2
2
2
 
3
+ # 1.27.1 / 2026-04-08
4
+
5
+ * Read markdown files (README, CHANGELOG) as UTF-8 to correctly render Unicode characters (e.g. umlauts, accented characters, emojis)
6
+
7
+ # 1.27.0 / 2026-04-01
8
+
9
+ * Adapt to Bunny 3.0 publisher confirms: use `confirm_select(tracking: true)` for automatic backpressure, remove manual `wait_for_confirms` calls
10
+ * Remove `:configuration` from default HTTP resources to prevent automatic exposure of sensitive config on upgrade (opt-in via `http_resources` method)
11
+ * 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
12
+ * Fix deprecation logic to only warn when `heartbeat.*` and `http.*` values actually differ; remove defaults from deprecated `heartbeat` config block
13
+ * Fix example processors: add unique HTTP ports, create missing `data/` directory in publisher
14
+ * Fix flaky sleeper specs by widening timing tolerance and using idiomatic RSpec `be_within` matcher
15
+
3
16
  # 1.26.2 / 2026-03-25
4
17
 
5
18
  * Change default `http.bind_address` from `localhost` to `0.0.0.0` for container compatibility (ECS, Docker, K8s)
data/README.md CHANGED
@@ -321,7 +321,7 @@ Resources are mounted under the `base_path`:
321
321
  - `{base_path}/heartbeat` - Health check
322
322
  - `{base_path}/version` - Version info as JSON
323
323
  - `{base_path}/docs` - README documentation as HTML
324
- - `{base_path}/docs/configuration` - Configuration as HTML table
324
+ - `{base_path}/docs/configuration` - Configuration as HTML table (opt-in, see [Enabling Resources](#enabling-resources))
325
325
  - `{base_path}/docs/changelog` - CHANGELOG as HTML
326
326
  - `{base_path}/assets/*` - Static assets (CSS, images)
327
327
 
@@ -401,6 +401,8 @@ GET {base_path}/docs
401
401
 
402
402
  **Response:** `200 OK` with HTML page
403
403
 
404
+ Markdown files are read as UTF-8, so Unicode characters (umlauts, accented characters, emojis, etc.) are rendered correctly.
405
+
404
406
  By default, looks for `README.md` in the current directory, then `doc/README.md`. You can customize the path via configuration:
405
407
 
406
408
  ```json
@@ -469,7 +471,7 @@ GET {base_path}/docs/configuration
469
471
 
470
472
  **Response:** `200 OK` with HTML page
471
473
 
472
- 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:
474
+ 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:
473
475
 
474
476
  ```ruby
475
477
  # Override the entire list
@@ -497,28 +499,22 @@ class MyProcessor < EventHub::Processor2
497
499
  end
498
500
  ```
499
501
 
500
- ### Disabling Resources
502
+ ### Enabling Resources
501
503
 
502
- 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.
504
+ 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.
503
505
 
504
- ```ruby
505
- class MyProcessor < EventHub::Processor2
506
- def http_resources
507
- [:heartbeat, :version, :docs, :changelog, :configuration] # default: all enabled
508
- end
509
- end
510
- ```
511
-
512
- To disable the configuration page for example:
506
+ To enable the configuration page, define an `http_resources` method in your processor:
513
507
 
514
508
  ```ruby
515
509
  class MyProcessor < EventHub::Processor2
516
510
  def http_resources
517
- [:heartbeat, :version, :docs, :changelog]
511
+ [:heartbeat, :version, :docs, :changelog, :configuration]
518
512
  end
519
513
  end
520
514
  ```
521
515
 
516
+ You can also use `http_resources` to disable any of the default resources. The navbar adapts automatically.
517
+
522
518
  ### Customizing Footer
523
519
 
524
520
  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,16 +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
186
+ # Prefer deprecated heartbeat config for backward compatibility,
187
+ # fall back to http config. Only warn when values actually differ.
187
188
  heartbeat_value = EventHub::Configuration.server.dig(:heartbeat, key)
188
189
  http_value = EventHub::Configuration.server.dig(:http, key)
189
190
 
190
- if heartbeat_value && http_value != heartbeat_value
191
+ if heartbeat_value && http_value && heartbeat_value != http_value
191
192
  EventHub.logger.warn("[DEPRECATION] heartbeat.#{key} is deprecated. Please use http.#{key} instead.")
192
193
  return heartbeat_value
193
194
  end
194
195
 
195
- http_value
196
+ http_value || heartbeat_value
196
197
  end
197
198
 
198
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;
@@ -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: "0.0.0.0",
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
@@ -35,7 +35,7 @@ module EventHub
35
35
  def asset(name)
36
36
  path = File.join(ASSETS_PATH, name)
37
37
  return nil unless File.exist?(path)
38
- File.read(path)
38
+ File.read(path, encoding: "utf-8")
39
39
  end
40
40
 
41
41
  private
@@ -63,7 +63,7 @@ module EventHub
63
63
  end
64
64
 
65
65
  if config_path && File.exist?(config_path)
66
- return File.read(config_path)
66
+ return File.read(config_path, encoding: "utf-8")
67
67
  end
68
68
 
69
69
  locations = case type
@@ -75,7 +75,7 @@ module EventHub
75
75
 
76
76
  locations.each do |location|
77
77
  path = File.join(Dir.pwd, location)
78
- return File.read(path) if File.exist?(path)
78
+ return File.read(path, encoding: "utf-8") if File.exist?(path)
79
79
  end
80
80
 
81
81
  "No #{(type == :readme) ? "README" : "CHANGELOG"} available."
@@ -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)
@@ -148,7 +265,7 @@ module EventHub
148
265
 
149
266
  def render_layout(title:, content:, content_class: "")
150
267
  template_path = File.join(TEMPLATES_PATH, "layout.erb")
151
- template = File.read(template_path)
268
+ template = File.read(template_path, encoding: "utf-8")
152
269
 
153
270
  processor_name = EventHub::Configuration.name
154
271
  version = processor_version
@@ -1,3 +1,3 @@
1
1
  module EventHub
2
- VERSION = "1.26.2".freeze
2
+ VERSION = "1.27.1".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.2
4
+ version: 1.27.1
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