jetstream_bridge 4.1.0 → 4.2.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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'integration'
4
+
5
+ module JetstreamBridge
6
+ # Rails integration for JetStream Bridge.
7
+ #
8
+ # This Railtie integrates JetStream Bridge with the Rails application lifecycle:
9
+ # - Configuration: Logger is configured early in the Rails boot process
10
+ # - Startup: Connection is established after user initializers load (explicit startup!)
11
+ # - Shutdown: Connection is closed when Rails shuts down (at_exit hook)
12
+ # - Restart: Puma/Unicorn workers get fresh connections on fork
13
+ #
14
+ class Railtie < ::Rails::Railtie
15
+ # Set up logger to use Rails.logger by default
16
+ # Note: This only configures the logger, does NOT establish connection
17
+ initializer 'jetstream_bridge.logger', before: :initialize_logger do
18
+ JetstreamBridge::Rails::Integration.configure_logger!
19
+ end
20
+
21
+ # Load ActiveRecord model tweaks after ActiveRecord is loaded
22
+ initializer 'jetstream_bridge.active_record', after: 'active_record.initialize_database' do
23
+ JetstreamBridge::Rails::Integration.attach_active_record_hooks!
24
+ end
25
+
26
+ # Establish connection after Rails initialization is complete
27
+ # This runs after all user initializers have loaded
28
+ config.after_initialize do
29
+ JetstreamBridge::Rails::Integration.boot_bridge!
30
+ end
31
+
32
+ # Add console helper methods
33
+ console do
34
+ ::Rails.logger.info "[JetStream Bridge] Loaded v#{JetstreamBridge::VERSION}"
35
+ ::Rails.logger.info '[JetStream Bridge] Console helpers available:'
36
+ ::Rails.logger.info ' JetstreamBridge.health_check - Check connection status'
37
+ ::Rails.logger.info ' JetstreamBridge.stream_info - View stream details'
38
+ ::Rails.logger.info ' JetstreamBridge.connected? - Check if connected'
39
+ ::Rails.logger.info ' JetstreamBridge.shutdown! - Gracefully disconnect'
40
+ ::Rails.logger.info ' JetstreamBridge.reconnect! - Reconnect (useful after configuration changes)'
41
+ end
42
+
43
+ # Load rake tasks
44
+ rake_tasks do
45
+ load File.expand_path('../tasks/install.rake', __dir__)
46
+ end
47
+
48
+ # Add generators
49
+ generators do
50
+ require 'generators/jetstream_bridge/health_check/health_check_generator'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point for Rails-specific integration (lifecycle helpers + railtie)
4
+ require_relative 'rails/integration'
5
+ require_relative 'rails/railtie' if defined?(Rails::Railtie)
@@ -85,7 +85,7 @@ namespace :jetstream_bridge do
85
85
  puts '[jetstream_bridge] Testing NATS connection...'
86
86
 
87
87
  begin
88
- jts = JetstreamBridge.ensure_topology!
88
+ jts = JetstreamBridge.connect_and_ensure_stream!
89
89
  puts '✓ Successfully connected to NATS'
90
90
  puts '✓ JetStream is available'
91
91
  puts '✓ Stream topology ensured'
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ module TestHelpers
5
+ # Common fixtures for quickly building events in specs.
6
+ module Fixtures
7
+ module_function
8
+
9
+ # Build a user.created event
10
+ def user_created_event(attrs = {})
11
+ JetstreamBridge::TestHelpers.build_jetstream_event(
12
+ event_type: 'user.created',
13
+ payload: {
14
+ id: attrs[:id] || 1,
15
+ email: attrs[:email] || 'test@example.com',
16
+ name: attrs[:name] || 'Test User'
17
+ }.merge(attrs[:payload] || {})
18
+ )
19
+ end
20
+
21
+ # Build multiple sample events
22
+ def sample_events(count = 3, type: 'test.event')
23
+ Array.new(count) do |i|
24
+ JetstreamBridge::TestHelpers.build_jetstream_event(
25
+ event_type: type,
26
+ payload: { id: i + 1, sequence: i }
27
+ )
28
+ end
29
+ end
30
+
31
+ # Build a generic event with custom attributes
32
+ def event(event_type:, payload: {}, **attrs)
33
+ JetstreamBridge::TestHelpers.build_jetstream_event(
34
+ event_type: event_type,
35
+ payload: payload,
36
+ **attrs
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ module TestHelpers
5
+ # Integration helpers to exercise the mock NATS storage end-to-end.
6
+ module IntegrationHelpers
7
+ # Publish an event and wait for it to appear in mock storage
8
+ #
9
+ # @param event_attrs [Hash] Event attributes to publish
10
+ # @param timeout [Integer] Maximum seconds to wait
11
+ # @return [Models::PublishResult] Publish result
12
+ # @raise [Timeout::Error] If event doesn't appear within timeout
13
+ def publish_and_wait(timeout: 1, **event_attrs)
14
+ result = JetstreamBridge.publish(**event_attrs)
15
+
16
+ deadline = Time.now + timeout
17
+ until Time.now > deadline
18
+ storage = JetstreamBridge::TestHelpers.mock_storage
19
+ break if storage.messages.any? { |m| m[:header]['nats-msg-id'] == result.event_id }
20
+
21
+ sleep 0.01
22
+ end
23
+
24
+ result
25
+ end
26
+
27
+ # Consume events from mock storage
28
+ #
29
+ # @param batch_size [Integer] Number of events to consume
30
+ # @yield [event] Block to handle each event
31
+ # @return [Array<Hash>] Consumed events
32
+ def consume_events(batch_size: 10, &handler)
33
+ storage = JetstreamBridge::TestHelpers.mock_storage
34
+ messages = storage.messages.first(batch_size)
35
+
36
+ messages.each do |msg|
37
+ event = JetstreamBridge::Models::Event.from_nats_message(
38
+ OpenStruct.new(
39
+ subject: msg[:subject],
40
+ data: msg[:data],
41
+ header: msg[:header],
42
+ metadata: OpenStruct.new(
43
+ sequence: OpenStruct.new(stream: msg[:sequence]),
44
+ num_delivered: msg[:delivery_count],
45
+ stream: 'test-stream',
46
+ consumer: 'test-consumer'
47
+ )
48
+ )
49
+ )
50
+
51
+ handler&.call(event)
52
+ JetstreamBridge::TestHelpers.record_consumed_event(event.to_h)
53
+ end
54
+
55
+ JetstreamBridge::TestHelpers.consumed_events
56
+ end
57
+
58
+ # Wait for a specific number of messages in mock storage
59
+ #
60
+ # @param count [Integer] Expected message count
61
+ # @param timeout [Integer] Maximum seconds to wait
62
+ # @return [Boolean] true if count reached, false if timeout
63
+ def wait_for_messages(count, timeout: 2)
64
+ deadline = Time.now + timeout
65
+ storage = JetstreamBridge::TestHelpers.mock_storage
66
+
67
+ until Time.now > deadline
68
+ return true if storage.messages.size >= count
69
+
70
+ sleep 0.01
71
+ end
72
+
73
+ false
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ module TestHelpers
5
+ # RSpec matchers for asserting publish outcomes and captured events.
6
+ module Matchers
7
+ # Matcher for checking if an event was published
8
+ #
9
+ # @param event_type [String] Event type to match
10
+ # @param payload [Hash] Optional payload attributes to match
11
+ # @return [HavePublished] Matcher instance
12
+ def have_published(event_type:, payload: {})
13
+ HavePublished.new(event_type, payload)
14
+ end
15
+
16
+ # Matcher implementation for have_published
17
+ class HavePublished
18
+ def initialize(event_type, payload_attributes)
19
+ @event_type = event_type
20
+ @payload_attributes = payload_attributes
21
+ end
22
+
23
+ def matches?(_actual)
24
+ TestHelpers.published_events.any? do |event|
25
+ matches_event_type?(event) && matches_payload?(event)
26
+ end
27
+ end
28
+
29
+ def failure_message
30
+ "expected to have published event_type: #{@event_type.inspect} " \
31
+ "with payload: #{@payload_attributes.inspect}\n" \
32
+ "but found events: #{TestHelpers.published_events.map { |e| e['event_type'] }.inspect}"
33
+ end
34
+
35
+ def failure_message_when_negated
36
+ "expected not to have published event_type: #{@event_type.inspect} " \
37
+ "with payload: #{@payload_attributes.inspect}"
38
+ end
39
+
40
+ private
41
+
42
+ def matches_event_type?(event)
43
+ event['event_type'] == @event_type || event[:event_type] == @event_type
44
+ end
45
+
46
+ def matches_payload?(event)
47
+ payload = event['payload'] || event[:payload] || {}
48
+ @payload_attributes.all? do |key, value|
49
+ payload_value = payload[key.to_s] || payload[key.to_sym]
50
+ if value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
51
+ value.matches?(payload)
52
+ else
53
+ payload_value == value
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # Matcher for checking publish result success
60
+ def be_publish_success
61
+ BePublishSuccess.new
62
+ end
63
+
64
+ class BePublishSuccess
65
+ def matches?(actual)
66
+ actual.respond_to?(:success?) && actual.success?
67
+ end
68
+
69
+ def failure_message
70
+ 'expected PublishResult to be successful but it failed'
71
+ end
72
+
73
+ def failure_message_when_negated
74
+ 'expected PublishResult to not be successful but it was'
75
+ end
76
+ end
77
+
78
+ # Matcher for checking publish result failure
79
+ def be_publish_failure
80
+ BePublishFailure.new
81
+ end
82
+
83
+ class BePublishFailure
84
+ def matches?(actual)
85
+ actual.respond_to?(:failure?) && actual.failure?
86
+ end
87
+
88
+ def failure_message
89
+ 'expected PublishResult to be a failure but it succeeded'
90
+ end
91
+
92
+ def failure_message_when_negated
93
+ 'expected PublishResult to not be a failure but it was'
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require_relative 'test_helpers/mock_nats'
5
+ require_relative 'test_helpers/matchers'
6
+ require_relative 'test_helpers/fixtures'
7
+ require_relative 'test_helpers/integration_helpers'
5
8
 
6
9
  module JetstreamBridge
7
10
  # Test helpers for easier testing of JetStream Bridge integrations
@@ -210,6 +213,7 @@ module JetstreamBridge
210
213
  }
211
214
  )
212
215
  end
216
+ module_function :build_jetstream_event
213
217
 
214
218
  # Simulate triggering an event to a consumer
215
219
  #
@@ -228,265 +232,6 @@ module JetstreamBridge
228
232
  TestHelpers.record_consumed_event(event.to_h) if TestHelpers.test_mode?
229
233
  handler.call(event)
230
234
  end
231
-
232
- # RSpec matchers module
233
- #
234
- # @example Include in RSpec
235
- # RSpec.configure do |config|
236
- # config.include JetstreamBridge::TestHelpers
237
- # config.include JetstreamBridge::TestHelpers::Matchers
238
- # end
239
- #
240
- module Matchers
241
- # Matcher for checking if an event was published
242
- #
243
- # @param event_type [String] Event type to match
244
- # @param payload [Hash] Optional payload attributes to match
245
- # @return [HavePublished] Matcher instance
246
- #
247
- # @example
248
- # expect(JetstreamBridge).to have_published(
249
- # event_type: "user.created",
250
- # payload: { id: 1 }
251
- # )
252
- #
253
- def have_published(event_type:, payload: {})
254
- HavePublished.new(event_type, payload)
255
- end
256
-
257
- # Matcher implementation for have_published
258
- class HavePublished
259
- def initialize(event_type, payload_attributes)
260
- @event_type = event_type
261
- @payload_attributes = payload_attributes
262
- end
263
-
264
- def matches?(_actual)
265
- TestHelpers.published_events.any? do |event|
266
- matches_event_type?(event) && matches_payload?(event)
267
- end
268
- end
269
-
270
- def failure_message
271
- "expected to have published event_type: #{@event_type.inspect} " \
272
- "with payload: #{@payload_attributes.inspect}\n" \
273
- "but found events: #{TestHelpers.published_events.map { |e| e['event_type'] }.inspect}"
274
- end
275
-
276
- def failure_message_when_negated
277
- "expected not to have published event_type: #{@event_type.inspect} " \
278
- "with payload: #{@payload_attributes.inspect}"
279
- end
280
-
281
- private
282
-
283
- def matches_event_type?(event)
284
- event['event_type'] == @event_type || event[:event_type] == @event_type
285
- end
286
-
287
- def matches_payload?(event)
288
- payload = event['payload'] || event[:payload] || {}
289
- @payload_attributes.all? do |key, value|
290
- payload_value = payload[key.to_s] || payload[key.to_sym]
291
- if value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
292
- value.matches?(payload)
293
- else
294
- payload_value == value
295
- end
296
- end
297
- end
298
- end
299
-
300
- # Matcher for checking publish result
301
- #
302
- # @example
303
- # result = JetstreamBridge.publish(...)
304
- # expect(result).to be_publish_success
305
- #
306
- def be_publish_success
307
- BePublishSuccess.new
308
- end
309
-
310
- # Matcher implementation for be_publish_success
311
- class BePublishSuccess
312
- def matches?(actual)
313
- actual.respond_to?(:success?) && actual.success?
314
- end
315
-
316
- def failure_message
317
- 'expected PublishResult to be successful but it failed'
318
- end
319
-
320
- def failure_message_when_negated
321
- 'expected PublishResult to not be successful but it was'
322
- end
323
- end
324
-
325
- # Matcher for checking publish failure
326
- #
327
- # @example
328
- # result = JetstreamBridge.publish(...)
329
- # expect(result).to be_publish_failure
330
- #
331
- def be_publish_failure
332
- BePublishFailure.new
333
- end
334
-
335
- # Matcher implementation for be_publish_failure
336
- class BePublishFailure
337
- def matches?(actual)
338
- actual.respond_to?(:failure?) && actual.failure?
339
- end
340
-
341
- def failure_message
342
- 'expected PublishResult to be a failure but it succeeded'
343
- end
344
-
345
- def failure_message_when_negated
346
- 'expected PublishResult to not be a failure but it was'
347
- end
348
- end
349
- end
350
-
351
- # Test fixtures for common event scenarios
352
- #
353
- # @example Create a user.created event
354
- # event = JetstreamBridge::TestHelpers::Fixtures.user_created_event(id: 123)
355
- #
356
- module Fixtures
357
- # Build a user.created event
358
- #
359
- # @param attrs [Hash] Event attributes
360
- # @option attrs [Integer] :id User ID
361
- # @option attrs [String] :email User email
362
- # @option attrs [String] :name User name
363
- # @option attrs [Hash] :payload Additional payload data
364
- # @return [Models::Event] Event object
365
- def self.user_created_event(attrs = {})
366
- build_jetstream_event(
367
- event_type: 'user.created',
368
- payload: {
369
- id: attrs[:id] || 1,
370
- email: attrs[:email] || 'test@example.com',
371
- name: attrs[:name] || 'Test User'
372
- }.merge(attrs[:payload] || {})
373
- )
374
- end
375
-
376
- # Build multiple sample events
377
- #
378
- # @param count [Integer] Number of events to create
379
- # @param type [String] Event type
380
- # @return [Array<Models::Event>] Array of event objects
381
- def self.sample_events(count = 3, type: 'test.event')
382
- Array.new(count) do |i|
383
- build_jetstream_event(
384
- event_type: type,
385
- payload: { id: i + 1, sequence: i }
386
- )
387
- end
388
- end
389
-
390
- # Build a generic event with custom attributes
391
- #
392
- # @param event_type [String] Event type
393
- # @param payload [Hash] Event payload
394
- # @param attrs [Hash] Additional event attributes
395
- # @return [Models::Event] Event object
396
- def self.event(event_type:, payload: {}, **attrs)
397
- build_jetstream_event(
398
- event_type: event_type,
399
- payload: payload,
400
- **attrs
401
- )
402
- end
403
-
404
- # Helper method to build events
405
- # @private
406
- def self.build_jetstream_event(event_type:, payload:, event_id: nil, trace_id: nil, occurred_at: nil, **metadata)
407
- # Delegate to main module method to avoid duplication
408
- TestHelpers.build_jetstream_event(
409
- event_type: event_type,
410
- payload: payload,
411
- event_id: event_id,
412
- trace_id: trace_id,
413
- occurred_at: occurred_at,
414
- **metadata
415
- )
416
- end
417
- end
418
-
419
- # Integration test helpers for end-to-end testing
420
- module IntegrationHelpers
421
- # Publish an event and wait for it to appear in mock storage
422
- #
423
- # @param event_attrs [Hash] Event attributes to publish
424
- # @param timeout [Integer] Maximum seconds to wait
425
- # @return [Models::PublishResult] Publish result
426
- # @raise [Timeout::Error] If event doesn't appear within timeout
427
- def publish_and_wait(event_attrs, timeout: 1)
428
- result = JetstreamBridge.publish(**event_attrs)
429
-
430
- deadline = Time.now + timeout
431
- until Time.now > deadline
432
- storage = JetstreamBridge::TestHelpers.mock_storage
433
- break if storage.messages.any? { |m| m[:header]['nats-msg-id'] == result.event_id }
434
-
435
- sleep 0.01
436
- end
437
-
438
- result
439
- end
440
-
441
- # Consume events from mock storage
442
- #
443
- # @param batch_size [Integer] Number of events to consume
444
- # @yield [event] Block to handle each event
445
- # @return [Array<Hash>] Consumed events
446
- def consume_events(batch_size: 10, &handler)
447
- storage = JetstreamBridge::TestHelpers.mock_storage
448
- messages = storage.messages.first(batch_size)
449
-
450
- messages.each do |msg|
451
- event = JetstreamBridge::Models::Event.from_nats_message(
452
- OpenStruct.new(
453
- subject: msg[:subject],
454
- data: msg[:data],
455
- header: msg[:header],
456
- metadata: OpenStruct.new(
457
- sequence: OpenStruct.new(stream: msg[:sequence]),
458
- num_delivered: msg[:delivery_count],
459
- stream: 'test-stream',
460
- consumer: 'test-consumer'
461
- )
462
- )
463
- )
464
-
465
- handler&.call(event)
466
- JetstreamBridge::TestHelpers.record_consumed_event(event.to_h)
467
- end
468
-
469
- JetstreamBridge::TestHelpers.consumed_events
470
- end
471
-
472
- # Wait for a specific number of messages in mock storage
473
- #
474
- # @param count [Integer] Expected message count
475
- # @param timeout [Integer] Maximum seconds to wait
476
- # @return [Boolean] true if count reached, false if timeout
477
- def wait_for_messages(count, timeout: 2)
478
- deadline = Time.now + timeout
479
- storage = JetstreamBridge::TestHelpers.mock_storage
480
-
481
- until Time.now > deadline
482
- return true if storage.messages.size >= count
483
-
484
- sleep 0.01
485
- end
486
-
487
- false
488
- end
489
- end
490
235
  end
491
236
  end
492
237
 
@@ -79,7 +79,7 @@ module JetstreamBridge
79
79
  def log_retention_mismatch(name, have:, want:)
80
80
  Logging.warn(
81
81
  "Stream #{name} retention mismatch (have=#{have.inspect}, want=#{want.inspect}). " \
82
- "Retention is immutable; skipping retention change.",
82
+ 'Retention is immutable; skipping retention change.',
83
83
  tag: 'JetstreamBridge::Stream'
84
84
  )
85
85
  end
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '4.1.0'
7
+ VERSION = '4.2.0'
8
8
  end