nats_pubsub 1.0.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 +7 -0
- data/exe/nats_pubsub +44 -0
- data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
- data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
- data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
- data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
- data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
- data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
- data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
- data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
- data/lib/nats_pubsub/active_record/publishable.rb +192 -0
- data/lib/nats_pubsub/cli.rb +105 -0
- data/lib/nats_pubsub/core/base_repository.rb +73 -0
- data/lib/nats_pubsub/core/config.rb +152 -0
- data/lib/nats_pubsub/core/config_presets.rb +139 -0
- data/lib/nats_pubsub/core/connection.rb +103 -0
- data/lib/nats_pubsub/core/constants.rb +190 -0
- data/lib/nats_pubsub/core/duration.rb +113 -0
- data/lib/nats_pubsub/core/error_action.rb +288 -0
- data/lib/nats_pubsub/core/event.rb +275 -0
- data/lib/nats_pubsub/core/health_check.rb +470 -0
- data/lib/nats_pubsub/core/logging.rb +72 -0
- data/lib/nats_pubsub/core/message_context.rb +193 -0
- data/lib/nats_pubsub/core/presets.rb +222 -0
- data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
- data/lib/nats_pubsub/core/structured_logger.rb +141 -0
- data/lib/nats_pubsub/core/subject.rb +185 -0
- data/lib/nats_pubsub/instrumentation.rb +327 -0
- data/lib/nats_pubsub/middleware/active_record.rb +18 -0
- data/lib/nats_pubsub/middleware/chain.rb +92 -0
- data/lib/nats_pubsub/middleware/logging.rb +48 -0
- data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
- data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
- data/lib/nats_pubsub/models/event_model.rb +73 -0
- data/lib/nats_pubsub/models/inbox_event.rb +109 -0
- data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
- data/lib/nats_pubsub/models/model_utils.rb +57 -0
- data/lib/nats_pubsub/models/outbox_event.rb +113 -0
- data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
- data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
- data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
- data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
- data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
- data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
- data/lib/nats_pubsub/publisher/publisher.rb +156 -0
- data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
- data/lib/nats_pubsub/railtie.rb +52 -0
- data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
- data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
- data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
- data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
- data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
- data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
- data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
- data/lib/nats_pubsub/subscribers/pool.rb +166 -0
- data/lib/nats_pubsub/subscribers/registry.rb +114 -0
- data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
- data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
- data/lib/nats_pubsub/subscribers/worker.rb +152 -0
- data/lib/nats_pubsub/tasks/install.rake +10 -0
- data/lib/nats_pubsub/testing/helpers.rb +199 -0
- data/lib/nats_pubsub/testing/matchers.rb +208 -0
- data/lib/nats_pubsub/testing/test_harness.rb +250 -0
- data/lib/nats_pubsub/testing.rb +157 -0
- data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
- data/lib/nats_pubsub/topology/stream.rb +102 -0
- data/lib/nats_pubsub/topology/stream_support.rb +170 -0
- data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
- data/lib/nats_pubsub/topology/topology.rb +24 -0
- data/lib/nats_pubsub/version.rb +8 -0
- data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
- data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
- data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
- data/lib/nats_pubsub/web/views/layout.erb +68 -0
- data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
- data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
- data/lib/nats_pubsub/web.rb +181 -0
- data/lib/nats_pubsub.rb +290 -0
- metadata +225 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNatsPubsubOutbox < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
# Disable DDL transaction for concurrent index creation
|
|
5
|
+
disable_ddl_transaction!
|
|
6
|
+
|
|
7
|
+
def up
|
|
8
|
+
# Idempotency check - safe to run multiple times
|
|
9
|
+
return if table_exists?(:nats_pubsub_outbox)
|
|
10
|
+
|
|
11
|
+
create_table :nats_pubsub_outbox do |t|
|
|
12
|
+
t.string :event_id, null: false
|
|
13
|
+
t.string :subject, null: false
|
|
14
|
+
t.jsonb :payload, null: false, default: {}
|
|
15
|
+
t.jsonb :headers, null: false, default: {}
|
|
16
|
+
t.string :status, null: false, default: 'pending' # pending|publishing|sent|failed
|
|
17
|
+
t.integer :attempts, null: false, default: 0
|
|
18
|
+
t.text :last_error
|
|
19
|
+
t.datetime :enqueued_at
|
|
20
|
+
t.datetime :sent_at
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Add indexes concurrently to avoid table locks
|
|
25
|
+
add_index :nats_pubsub_outbox, :event_id,
|
|
26
|
+
unique: true,
|
|
27
|
+
algorithm: :concurrently,
|
|
28
|
+
if_not_exists: true
|
|
29
|
+
|
|
30
|
+
add_index :nats_pubsub_outbox, :status,
|
|
31
|
+
algorithm: :concurrently,
|
|
32
|
+
if_not_exists: true
|
|
33
|
+
|
|
34
|
+
# Composite index for common queries (status + created_at)
|
|
35
|
+
add_index :nats_pubsub_outbox, [:status, :created_at],
|
|
36
|
+
algorithm: :concurrently,
|
|
37
|
+
if_not_exists: true,
|
|
38
|
+
name: 'index_outbox_on_status_and_created'
|
|
39
|
+
|
|
40
|
+
# Composite index for status + enqueued_at queries
|
|
41
|
+
add_index :nats_pubsub_outbox, [:status, :enqueued_at],
|
|
42
|
+
algorithm: :concurrently,
|
|
43
|
+
if_not_exists: true,
|
|
44
|
+
name: 'index_outbox_on_status_and_enqueued'
|
|
45
|
+
|
|
46
|
+
# Partial index for sent events (for cleanup queries)
|
|
47
|
+
add_index :nats_pubsub_outbox, :sent_at,
|
|
48
|
+
where: "status = 'sent'",
|
|
49
|
+
algorithm: :concurrently,
|
|
50
|
+
if_not_exists: true,
|
|
51
|
+
name: 'index_outbox_sent_at'
|
|
52
|
+
|
|
53
|
+
# Partial index for stale publishing records
|
|
54
|
+
add_index :nats_pubsub_outbox, :updated_at,
|
|
55
|
+
where: "status = 'publishing'",
|
|
56
|
+
algorithm: :concurrently,
|
|
57
|
+
if_not_exists: true,
|
|
58
|
+
name: 'index_outbox_stale_publishing'
|
|
59
|
+
|
|
60
|
+
# GIN index for JSONB payload queries (PostgreSQL only)
|
|
61
|
+
if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgres')
|
|
62
|
+
add_index :nats_pubsub_outbox, :payload,
|
|
63
|
+
using: :gin,
|
|
64
|
+
algorithm: :concurrently,
|
|
65
|
+
if_not_exists: true,
|
|
66
|
+
name: 'index_outbox_payload_gin'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Database-level constraint for status values
|
|
70
|
+
execute <<-SQL
|
|
71
|
+
ALTER TABLE nats_pubsub_outbox
|
|
72
|
+
ADD CONSTRAINT check_outbox_status_values
|
|
73
|
+
CHECK (status IN ('pending', 'publishing', 'sent', 'failed'))
|
|
74
|
+
SQL
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def down
|
|
78
|
+
# Safe rollback with checks
|
|
79
|
+
drop_table :nats_pubsub_outbox if table_exists?(:nats_pubsub_outbox)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module NatsPubsub
|
|
6
|
+
module Generators
|
|
7
|
+
# Subscriber generator that creates a new NatsPubsub subscriber class
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# rails generate nats_pubsub:subscriber NAME [topic1 topic2...] [options]
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# rails generate nats_pubsub:subscriber UserNotification
|
|
14
|
+
# rails generate nats_pubsub:subscriber OrderProcessor orders.order
|
|
15
|
+
# rails generate nats_pubsub:subscriber EmailHandler notifications.email --wildcard
|
|
16
|
+
# rails generate nats_pubsub:subscriber AuditLogger --topics=audit.user audit.order
|
|
17
|
+
#
|
|
18
|
+
# Options:
|
|
19
|
+
# --topics=one two three Specify topics to subscribe to
|
|
20
|
+
# --wildcard Use wildcard subscription (topic.>)
|
|
21
|
+
# --skip-test Skip test file generation
|
|
22
|
+
#
|
|
23
|
+
# This will create:
|
|
24
|
+
# app/subscribers/user_notification_subscriber.rb
|
|
25
|
+
# spec/subscribers/user_notification_subscriber_spec.rb (if RSpec is detected)
|
|
26
|
+
#
|
|
27
|
+
# The generated subscriber will:
|
|
28
|
+
# - Include NatsPubsub::Subscriber module
|
|
29
|
+
# - Subscribe to specified topics
|
|
30
|
+
# - Implement handle method stub
|
|
31
|
+
# - Include error handling example
|
|
32
|
+
class SubscriberGenerator < Rails::Generators::NamedBase
|
|
33
|
+
source_root File.expand_path('templates', __dir__)
|
|
34
|
+
desc 'Creates a NatsPubsub subscriber class'
|
|
35
|
+
|
|
36
|
+
argument :topics_list, type: :array, default: [], banner: 'topic1 topic2...'
|
|
37
|
+
|
|
38
|
+
class_option :topics, type: :array, default: [],
|
|
39
|
+
desc: 'Topics to subscribe to (alternative to positional args)'
|
|
40
|
+
class_option :wildcard, type: :boolean, default: false,
|
|
41
|
+
desc: 'Use wildcard subscription (topic.>)'
|
|
42
|
+
class_option :skip_test, type: :boolean, default: false,
|
|
43
|
+
desc: 'Skip test file generation'
|
|
44
|
+
|
|
45
|
+
def create_subscriber_file
|
|
46
|
+
template 'subscriber.rb.tt', File.join('app/subscribers', class_path, "#{file_name}_subscriber.rb")
|
|
47
|
+
say_status :created, "app/subscribers/#{file_name}_subscriber.rb", :green
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
say_status :error, "Failed to create subscriber: #{e.message}", :red
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_test_file
|
|
54
|
+
return if options[:skip_test]
|
|
55
|
+
|
|
56
|
+
if rspec_detected?
|
|
57
|
+
create_rspec_file
|
|
58
|
+
elsif test_unit_detected?
|
|
59
|
+
create_test_unit_file
|
|
60
|
+
else
|
|
61
|
+
say_status :skipped, 'No test framework detected', :yellow
|
|
62
|
+
end
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
say_status :error, "Failed to create test: #{e.message}", :red
|
|
65
|
+
# Don't raise - test generation failure shouldn't stop subscriber creation
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def create_rspec_file
|
|
71
|
+
template 'subscriber_spec.rb.tt',
|
|
72
|
+
File.join('spec/subscribers', class_path, "#{file_name}_subscriber_spec.rb")
|
|
73
|
+
say_status :created, "spec/subscribers/#{file_name}_subscriber_spec.rb", :green
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_test_unit_file
|
|
77
|
+
template 'subscriber_test.rb.tt',
|
|
78
|
+
File.join('test/subscribers', class_path, "#{file_name}_subscriber_test.rb")
|
|
79
|
+
say_status :created, "test/subscribers/#{file_name}_subscriber_test.rb", :green
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rspec_detected?
|
|
83
|
+
File.exist?(File.join(destination_root, 'spec', 'spec_helper.rb')) ||
|
|
84
|
+
File.exist?(File.join(destination_root, 'spec', 'rails_helper.rb'))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_unit_detected?
|
|
88
|
+
File.exist?(File.join(destination_root, 'test', 'test_helper.rb'))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get all topics from both positional args and --topics option
|
|
92
|
+
def all_topics
|
|
93
|
+
combined = topics_list + options[:topics]
|
|
94
|
+
combined.empty? ? default_topics : combined.uniq
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Default topics based on subscriber name
|
|
98
|
+
def default_topics
|
|
99
|
+
[file_name.pluralize.tr('_', '.')]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if using wildcard subscription
|
|
103
|
+
def use_wildcard?
|
|
104
|
+
options[:wildcard]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Generate subscription code
|
|
108
|
+
def subscription_code
|
|
109
|
+
if all_topics.empty?
|
|
110
|
+
" # subscribe_to 'your.topic'\n # subscribe_to_wildcard 'your.topic'"
|
|
111
|
+
elsif use_wildcard?
|
|
112
|
+
all_topics.map { |topic| " subscribe_to_wildcard '#{topic}'" }.join("\n")
|
|
113
|
+
else
|
|
114
|
+
all_topics.map { |topic| " subscribe_to '#{topic}'" }.join("\n")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Generate example topic for comments
|
|
119
|
+
def example_topic
|
|
120
|
+
all_topics.first || 'your.topic'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check if subscriber name ends with 'Subscriber'
|
|
124
|
+
def needs_subscriber_suffix?
|
|
125
|
+
!class_name.end_with?('Subscriber')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Get correct class name with Subscriber suffix
|
|
129
|
+
def subscriber_class_name
|
|
130
|
+
needs_subscriber_suffix? ? "#{class_name}Subscriber" : class_name
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get correct file name
|
|
134
|
+
def subscriber_file_name
|
|
135
|
+
needs_subscriber_suffix? ? "#{file_name}_subscriber" : file_name
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <%= subscriber_class_name %> handles messages from NATS topics
|
|
4
|
+
#
|
|
5
|
+
# This subscriber listens to the following topic(s):
|
|
6
|
+
<% all_topics.each do |topic| -%>
|
|
7
|
+
# - <%= topic %><%= use_wildcard? ? '.>' : '' %>
|
|
8
|
+
<% end -%>
|
|
9
|
+
#
|
|
10
|
+
# Message format:
|
|
11
|
+
# {
|
|
12
|
+
# "event_id": "uuid",
|
|
13
|
+
# "domain": "domain_name",
|
|
14
|
+
# "resource": "resource_name",
|
|
15
|
+
# "action": "action_name",
|
|
16
|
+
# "data": { ... }
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# Context includes:
|
|
20
|
+
# - event_id: Unique event identifier
|
|
21
|
+
# - trace_id: Distributed tracing ID
|
|
22
|
+
# - deliveries: Number of delivery attempts
|
|
23
|
+
# - topic: The topic this message was published to
|
|
24
|
+
# - subject: Full NATS subject (env.app.domain.resource.action)
|
|
25
|
+
class <%= subscriber_class_name %>
|
|
26
|
+
include NatsPubsub::Subscriber
|
|
27
|
+
|
|
28
|
+
# Subscribe to topics
|
|
29
|
+
<%= subscription_code %>
|
|
30
|
+
|
|
31
|
+
# Configure JetStream options (optional)
|
|
32
|
+
# jetstream_options retry: 5, ack_wait: 30, max_deliver: 5
|
|
33
|
+
|
|
34
|
+
# Handle incoming messages
|
|
35
|
+
#
|
|
36
|
+
# @param message [Hash] The message payload
|
|
37
|
+
# @param context [NatsPubsub::Core::MessageContext] Message context
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# {
|
|
42
|
+
# "event_id" => "uuid",
|
|
43
|
+
# "domain" => "<%= example_topic.split('.').first %>",
|
|
44
|
+
# "resource" => "<%= example_topic.split('.')[1] || 'resource' %>",
|
|
45
|
+
# "action" => "created",
|
|
46
|
+
# "data" => { "id" => 1, "name" => "Example" }
|
|
47
|
+
# }
|
|
48
|
+
def handle(message, context)
|
|
49
|
+
logger.info "Processing message: event_id=#{context.event_id} topic=#{context.topic}"
|
|
50
|
+
|
|
51
|
+
# Extract data from message
|
|
52
|
+
data = message['data'] || message
|
|
53
|
+
event_id = message['event_id']
|
|
54
|
+
action = message['action']
|
|
55
|
+
|
|
56
|
+
# TODO: Implement your message processing logic here
|
|
57
|
+
# Example:
|
|
58
|
+
# case action
|
|
59
|
+
# when 'created'
|
|
60
|
+
# handle_created(data, context)
|
|
61
|
+
# when 'updated'
|
|
62
|
+
# handle_updated(data, context)
|
|
63
|
+
# when 'deleted'
|
|
64
|
+
# handle_deleted(data, context)
|
|
65
|
+
# else
|
|
66
|
+
# logger.warn "Unknown action: #{action}"
|
|
67
|
+
# end
|
|
68
|
+
|
|
69
|
+
logger.info "Successfully processed: event_id=#{context.event_id}"
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
logger.error "Failed to process message: #{e.class}: #{e.message}"
|
|
72
|
+
logger.error e.backtrace.join("\n")
|
|
73
|
+
raise # Re-raise to trigger retry/DLQ logic
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Optional: Custom error handling
|
|
77
|
+
#
|
|
78
|
+
# Return an error action to control how errors are handled:
|
|
79
|
+
# - NatsPubsub::Core::ErrorAction::RETRY - Retry the message (default)
|
|
80
|
+
# - NatsPubsub::Core::ErrorAction::DISCARD - Discard the message
|
|
81
|
+
# - NatsPubsub::Core::ErrorAction::DLQ - Send to dead letter queue
|
|
82
|
+
#
|
|
83
|
+
# @param error_context [NatsPubsub::Core::ErrorContext] Error context
|
|
84
|
+
# @return [Symbol] Error action
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# def on_error(error_context)
|
|
88
|
+
# case error_context.error
|
|
89
|
+
# when ActiveRecord::RecordNotFound
|
|
90
|
+
# NatsPubsub::Core::ErrorAction::DISCARD
|
|
91
|
+
# when Timeout::Error
|
|
92
|
+
# NatsPubsub::Core::ErrorAction::RETRY
|
|
93
|
+
# else
|
|
94
|
+
# NatsPubsub::Core::ErrorAction::DLQ
|
|
95
|
+
# end
|
|
96
|
+
# end
|
|
97
|
+
# def on_error(error_context)
|
|
98
|
+
# super
|
|
99
|
+
# end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Example helper methods for different actions
|
|
104
|
+
# Uncomment and implement as needed
|
|
105
|
+
|
|
106
|
+
# def handle_created(data, context)
|
|
107
|
+
# # Handle created events
|
|
108
|
+
# end
|
|
109
|
+
|
|
110
|
+
# def handle_updated(data, context)
|
|
111
|
+
# # Handle updated events
|
|
112
|
+
# end
|
|
113
|
+
|
|
114
|
+
# def handle_deleted(data, context)
|
|
115
|
+
# # Handle deleted events
|
|
116
|
+
# end
|
|
117
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe <%= subscriber_class_name %>, nats_fake: true do
|
|
6
|
+
subject(:subscriber) { described_class.new }
|
|
7
|
+
|
|
8
|
+
describe '.all_subscriptions' do
|
|
9
|
+
it 'subscribes to correct topics' do
|
|
10
|
+
<% all_topics.each do |topic| -%>
|
|
11
|
+
expect(described_class.all_subscriptions).to include(
|
|
12
|
+
hash_including(topic: '<%= topic %>')
|
|
13
|
+
)
|
|
14
|
+
<% end -%>
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#handle' do
|
|
19
|
+
let(:event_id) { SecureRandom.uuid }
|
|
20
|
+
let(:trace_id) { SecureRandom.uuid }
|
|
21
|
+
let(:context) do
|
|
22
|
+
NatsPubsub::Core::MessageContext.new(
|
|
23
|
+
event_id: event_id,
|
|
24
|
+
trace_id: trace_id,
|
|
25
|
+
topic: '<%= example_topic %>',
|
|
26
|
+
subject: 'test.app.<%= example_topic %>',
|
|
27
|
+
deliveries: 1,
|
|
28
|
+
timestamp: Time.current
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'with valid message' do
|
|
33
|
+
let(:message) do
|
|
34
|
+
{
|
|
35
|
+
'event_id' => event_id,
|
|
36
|
+
'domain' => '<%= example_topic.split('.').first %>',
|
|
37
|
+
'resource' => '<%= example_topic.split('.')[1] || 'resource' %>',
|
|
38
|
+
'action' => 'created',
|
|
39
|
+
'data' => {
|
|
40
|
+
'id' => 1,
|
|
41
|
+
'name' => 'Test <%= class_name %>'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'processes the message successfully' do
|
|
47
|
+
expect { subscriber.handle(message, context) }.not_to raise_error
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'logs processing information' do
|
|
51
|
+
allow(Rails.logger).to receive(:info)
|
|
52
|
+
|
|
53
|
+
subscriber.handle(message, context)
|
|
54
|
+
|
|
55
|
+
expect(Rails.logger).to have_received(:info)
|
|
56
|
+
.with(/Processing message: event_id=#{event_id}/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# TODO: Add specific assertions for your business logic
|
|
60
|
+
# it 'creates the expected record' do
|
|
61
|
+
# expect { subscriber.handle(message, context) }
|
|
62
|
+
# .to change { YourModel.count }.by(1)
|
|
63
|
+
# end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'with invalid message' do
|
|
67
|
+
let(:message) { {} }
|
|
68
|
+
|
|
69
|
+
it 'handles missing data gracefully' do
|
|
70
|
+
# TODO: Implement based on your error handling strategy
|
|
71
|
+
# expect { subscriber.handle(message, context) }.not_to raise_error
|
|
72
|
+
# OR
|
|
73
|
+
# expect { subscriber.handle(message, context) }.to raise_error(StandardError)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context 'when processing fails' do
|
|
78
|
+
let(:message) do
|
|
79
|
+
{
|
|
80
|
+
'event_id' => event_id,
|
|
81
|
+
'action' => 'created',
|
|
82
|
+
'data' => { 'id' => 1 }
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
before do
|
|
87
|
+
# Simulate a processing failure
|
|
88
|
+
# allow(YourService).to receive(:call).and_raise(StandardError, 'Processing failed')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'logs the error' do
|
|
92
|
+
# TODO: Implement based on your error handling
|
|
93
|
+
# allow(Rails.logger).to receive(:error)
|
|
94
|
+
# expect { subscriber.handle(message, context) }.to raise_error(StandardError)
|
|
95
|
+
# expect(Rails.logger).to have_received(:error)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Optional: Test custom error handling
|
|
101
|
+
# describe '#on_error' do
|
|
102
|
+
# let(:error_context) do
|
|
103
|
+
# NatsPubsub::Core::ErrorContext.new(
|
|
104
|
+
# error: StandardError.new('Test error'),
|
|
105
|
+
# message: {},
|
|
106
|
+
# context: double(event_id: 'test-id'),
|
|
107
|
+
# attempts: 1
|
|
108
|
+
# )
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# it 'returns appropriate error action' do
|
|
112
|
+
# result = subscriber.on_error(error_context)
|
|
113
|
+
# expect(result).to eq(NatsPubsub::Core::ErrorAction::RETRY)
|
|
114
|
+
# end
|
|
115
|
+
# end
|
|
116
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class <%= subscriber_class_name %>Test < ActiveSupport::TestCase
|
|
6
|
+
def setup
|
|
7
|
+
@subscriber = <%= subscriber_class_name %>.new
|
|
8
|
+
@event_id = SecureRandom.uuid
|
|
9
|
+
@trace_id = SecureRandom.uuid
|
|
10
|
+
@context = NatsPubsub::Core::MessageContext.new(
|
|
11
|
+
event_id: @event_id,
|
|
12
|
+
trace_id: @trace_id,
|
|
13
|
+
topic: '<%= example_topic %>',
|
|
14
|
+
subject: 'test.app.<%= example_topic %>',
|
|
15
|
+
deliveries: 1,
|
|
16
|
+
timestamp: Time.current
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Enable fake mode for testing
|
|
20
|
+
NatsPubsub::Testing.fake!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def teardown
|
|
24
|
+
NatsPubsub::Testing.clear!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
test 'subscribes to correct topics' do
|
|
28
|
+
subscriptions = <%= subscriber_class_name %>.all_subscriptions
|
|
29
|
+
<% all_topics.each do |topic| -%>
|
|
30
|
+
assert_includes subscriptions.map { |s| s[:topic] }, '<%= topic %>'
|
|
31
|
+
<% end -%>
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
test 'processes valid message successfully' do
|
|
35
|
+
message = {
|
|
36
|
+
'event_id' => @event_id,
|
|
37
|
+
'domain' => '<%= example_topic.split('.').first %>',
|
|
38
|
+
'resource' => '<%= example_topic.split('.')[1] || 'resource' %>',
|
|
39
|
+
'action' => 'created',
|
|
40
|
+
'data' => {
|
|
41
|
+
'id' => 1,
|
|
42
|
+
'name' => 'Test <%= class_name %>'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert_nothing_raised do
|
|
47
|
+
@subscriber.handle(message, @context)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test 'logs processing information' do
|
|
52
|
+
message = {
|
|
53
|
+
'event_id' => @event_id,
|
|
54
|
+
'action' => 'created',
|
|
55
|
+
'data' => { 'id' => 1 }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Rails.logger.expects(:info).with(regexp_matches(/Processing message/))
|
|
59
|
+
@subscriber.handle(message, @context)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# TODO: Add specific test cases for your business logic
|
|
63
|
+
# test 'creates expected record' do
|
|
64
|
+
# message = {
|
|
65
|
+
# 'event_id' => @event_id,
|
|
66
|
+
# 'action' => 'created',
|
|
67
|
+
# 'data' => { 'id' => 1, 'name' => 'Test' }
|
|
68
|
+
# }
|
|
69
|
+
#
|
|
70
|
+
# assert_difference 'YourModel.count', 1 do
|
|
71
|
+
# @subscriber.handle(message, @context)
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
|
|
75
|
+
test 'handles missing data' do
|
|
76
|
+
message = {}
|
|
77
|
+
|
|
78
|
+
# TODO: Implement based on your error handling strategy
|
|
79
|
+
# assert_nothing_raised do
|
|
80
|
+
# @subscriber.handle(message, @context)
|
|
81
|
+
# end
|
|
82
|
+
# OR
|
|
83
|
+
# assert_raises StandardError do
|
|
84
|
+
# @subscriber.handle(message, @context)
|
|
85
|
+
# end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
test 'logs errors when processing fails' do
|
|
89
|
+
message = {
|
|
90
|
+
'event_id' => @event_id,
|
|
91
|
+
'action' => 'created',
|
|
92
|
+
'data' => { 'id' => 1 }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Simulate a processing failure
|
|
96
|
+
# YourService.stubs(:call).raises(StandardError, 'Processing failed')
|
|
97
|
+
|
|
98
|
+
# TODO: Implement based on your error handling
|
|
99
|
+
# Rails.logger.expects(:error).at_least_once
|
|
100
|
+
# assert_raises StandardError do
|
|
101
|
+
# @subscriber.handle(message, @context)
|
|
102
|
+
# end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Optional: Test custom error handling
|
|
106
|
+
# test 'returns appropriate error action' do
|
|
107
|
+
# error_context = NatsPubsub::Core::ErrorContext.new(
|
|
108
|
+
# error: StandardError.new('Test error'),
|
|
109
|
+
# message: {},
|
|
110
|
+
# context: stub(event_id: 'test-id'),
|
|
111
|
+
# attempts: 1
|
|
112
|
+
# )
|
|
113
|
+
#
|
|
114
|
+
# result = @subscriber.on_error(error_context)
|
|
115
|
+
# assert_equal NatsPubsub::Core::ErrorAction::RETRY, result
|
|
116
|
+
# end
|
|
117
|
+
end
|