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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/exe/nats_pubsub +44 -0
  3. data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
  4. data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
  5. data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
  6. data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
  7. data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
  8. data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
  9. data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
  10. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
  11. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
  12. data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
  13. data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
  14. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
  15. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
  16. data/lib/nats_pubsub/active_record/publishable.rb +192 -0
  17. data/lib/nats_pubsub/cli.rb +105 -0
  18. data/lib/nats_pubsub/core/base_repository.rb +73 -0
  19. data/lib/nats_pubsub/core/config.rb +152 -0
  20. data/lib/nats_pubsub/core/config_presets.rb +139 -0
  21. data/lib/nats_pubsub/core/connection.rb +103 -0
  22. data/lib/nats_pubsub/core/constants.rb +190 -0
  23. data/lib/nats_pubsub/core/duration.rb +113 -0
  24. data/lib/nats_pubsub/core/error_action.rb +288 -0
  25. data/lib/nats_pubsub/core/event.rb +275 -0
  26. data/lib/nats_pubsub/core/health_check.rb +470 -0
  27. data/lib/nats_pubsub/core/logging.rb +72 -0
  28. data/lib/nats_pubsub/core/message_context.rb +193 -0
  29. data/lib/nats_pubsub/core/presets.rb +222 -0
  30. data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
  31. data/lib/nats_pubsub/core/structured_logger.rb +141 -0
  32. data/lib/nats_pubsub/core/subject.rb +185 -0
  33. data/lib/nats_pubsub/instrumentation.rb +327 -0
  34. data/lib/nats_pubsub/middleware/active_record.rb +18 -0
  35. data/lib/nats_pubsub/middleware/chain.rb +92 -0
  36. data/lib/nats_pubsub/middleware/logging.rb +48 -0
  37. data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
  38. data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
  39. data/lib/nats_pubsub/models/event_model.rb +73 -0
  40. data/lib/nats_pubsub/models/inbox_event.rb +109 -0
  41. data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
  42. data/lib/nats_pubsub/models/model_utils.rb +57 -0
  43. data/lib/nats_pubsub/models/outbox_event.rb +113 -0
  44. data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
  45. data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
  46. data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
  47. data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
  48. data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
  49. data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
  50. data/lib/nats_pubsub/publisher/publisher.rb +156 -0
  51. data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
  52. data/lib/nats_pubsub/railtie.rb +52 -0
  53. data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
  54. data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
  55. data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
  56. data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
  57. data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
  58. data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
  59. data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
  60. data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
  61. data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
  62. data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
  63. data/lib/nats_pubsub/subscribers/pool.rb +166 -0
  64. data/lib/nats_pubsub/subscribers/registry.rb +114 -0
  65. data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
  66. data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
  67. data/lib/nats_pubsub/subscribers/worker.rb +152 -0
  68. data/lib/nats_pubsub/tasks/install.rake +10 -0
  69. data/lib/nats_pubsub/testing/helpers.rb +199 -0
  70. data/lib/nats_pubsub/testing/matchers.rb +208 -0
  71. data/lib/nats_pubsub/testing/test_harness.rb +250 -0
  72. data/lib/nats_pubsub/testing.rb +157 -0
  73. data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
  74. data/lib/nats_pubsub/topology/stream.rb +102 -0
  75. data/lib/nats_pubsub/topology/stream_support.rb +170 -0
  76. data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
  77. data/lib/nats_pubsub/topology/topology.rb +24 -0
  78. data/lib/nats_pubsub/version.rb +8 -0
  79. data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
  80. data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
  81. data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
  82. data/lib/nats_pubsub/web/views/layout.erb +68 -0
  83. data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
  84. data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
  85. data/lib/nats_pubsub/web.rb +181 -0
  86. data/lib/nats_pubsub.rb +290 -0
  87. metadata +225 -0
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module NatsPubsub
6
+ # Immutable value object representing a domain event or message.
7
+ # Provides a structured interface for accessing event envelope data.
8
+ #
9
+ # Events follow the envelope pattern with metadata and payload:
10
+ # - Metadata: event_id, timestamp, producer, domain/resource/action or topic
11
+ # - Payload: The actual event data
12
+ #
13
+ # @example Creating an event from envelope
14
+ # envelope = {
15
+ # 'event_id' => '123',
16
+ # 'domain' => 'users',
17
+ # 'resource' => 'user',
18
+ # 'action' => 'created',
19
+ # 'payload' => { 'id' => 1, 'name' => 'John' },
20
+ # 'occurred_at' => '2025-01-01T00:00:00Z'
21
+ # }
22
+ # event = Event.from_envelope(envelope)
23
+ # event.domain # => 'users'
24
+ # event.payload # => { 'id' => 1, 'name' => 'John' }
25
+ class Event
26
+ attr_reader :event_id, :schema_version, :payload, :occurred_at,
27
+ :trace_id, :producer, :resource_id
28
+
29
+ # Domain/Resource/Action fields (for event-based messages)
30
+ attr_reader :domain, :resource, :action
31
+
32
+ # Topic field (for topic-based messages)
33
+ attr_reader :topic, :message_type
34
+
35
+ # Create event from envelope hash
36
+ #
37
+ # @param envelope [Hash] Event envelope
38
+ # @return [Event] New event instance
39
+ # @raise [ArgumentError] if envelope is missing required fields
40
+ def self.from_envelope(envelope)
41
+ new(
42
+ event_id: envelope['event_id'] || envelope[:event_id],
43
+ schema_version: envelope['schema_version'] || envelope[:schema_version] || 1,
44
+ domain: envelope['domain'] || envelope[:domain],
45
+ resource: envelope['resource'] || envelope[:resource],
46
+ action: envelope['action'] || envelope[:action],
47
+ topic: envelope['topic'] || envelope[:topic],
48
+ message_type: envelope['message_type'] || envelope[:message_type],
49
+ producer: envelope['producer'] || envelope[:producer],
50
+ resource_id: envelope['resource_id'] || envelope[:resource_id],
51
+ occurred_at: parse_timestamp(envelope['occurred_at'] || envelope[:occurred_at]),
52
+ trace_id: envelope['trace_id'] || envelope[:trace_id],
53
+ payload: extract_payload(envelope)
54
+ )
55
+ end
56
+
57
+ # Initialize a new Event
58
+ #
59
+ # @param event_id [String] Unique event identifier
60
+ # @param schema_version [Integer] Event schema version
61
+ # @param payload [Hash] Event payload data
62
+ # @param occurred_at [Time] When event occurred
63
+ # @param producer [String, nil] Application that produced the event
64
+ # @param trace_id [String, nil] Distributed tracing ID
65
+ # @param resource_id [String, nil] ID of the resource
66
+ # @param domain [String, nil] Domain name (event-based)
67
+ # @param resource [String, nil] Resource type (event-based)
68
+ # @param action [String, nil] Action performed (event-based)
69
+ # @param topic [String, nil] Topic name (topic-based)
70
+ # @param message_type [String, nil] Message type (topic-based)
71
+ def initialize(event_id:, schema_version: 1, payload:, occurred_at: nil,
72
+ producer: nil, trace_id: nil, resource_id: nil,
73
+ domain: nil, resource: nil, action: nil,
74
+ topic: nil, message_type: nil)
75
+ @event_id = event_id.to_s
76
+ @schema_version = schema_version.to_i
77
+ @payload = payload || {}
78
+ @occurred_at = occurred_at || Time.now.utc
79
+ @producer = producer
80
+ @trace_id = trace_id
81
+ @resource_id = resource_id
82
+
83
+ # Event-based fields
84
+ @domain = domain
85
+ @resource = resource
86
+ @action = action
87
+
88
+ # Topic-based fields
89
+ @topic = topic
90
+ @message_type = message_type
91
+
92
+ validate!
93
+ freeze
94
+ end
95
+
96
+ # Check if this is an event-based message
97
+ #
98
+ # @return [Boolean] True if has domain/resource/action
99
+ def event_based?
100
+ !@domain.nil? && !@resource.nil? && !@action.nil?
101
+ end
102
+
103
+ # Check if this is a topic-based message
104
+ #
105
+ # @return [Boolean] True if has topic
106
+ def topic_based?
107
+ !@topic.nil?
108
+ end
109
+
110
+ # Get event type identifier
111
+ # Returns domain.resource.action for events, or topic for topic messages
112
+ #
113
+ # @return [String] Event type
114
+ def event_type
115
+ if event_based?
116
+ "#{@domain}.#{@resource}.#{@action}"
117
+ elsif topic_based?
118
+ @topic
119
+ else
120
+ 'unknown'
121
+ end
122
+ end
123
+
124
+ # Check if event matches domain/resource/action pattern
125
+ #
126
+ # @param domain [String, nil] Domain to match (nil matches any)
127
+ # @param resource [String, nil] Resource to match (nil matches any)
128
+ # @param action [String, nil] Action to match (nil matches any)
129
+ # @return [Boolean] True if matches
130
+ def matches_event?(domain: nil, resource: nil, action: nil)
131
+ return false unless event_based?
132
+
133
+ (domain.nil? || @domain == domain.to_s) &&
134
+ (resource.nil? || @resource == resource.to_s) &&
135
+ (action.nil? || @action == action.to_s)
136
+ end
137
+
138
+ # Check if event matches topic pattern
139
+ #
140
+ # @param topic [String] Topic to match
141
+ # @return [Boolean] True if matches
142
+ def matches_topic?(topic)
143
+ return false unless topic_based?
144
+
145
+ @topic == topic.to_s
146
+ end
147
+
148
+ # Convert event to envelope hash (for serialization)
149
+ #
150
+ # @return [Hash] Event envelope
151
+ def to_envelope
152
+ envelope = {
153
+ 'event_id' => @event_id,
154
+ 'schema_version' => @schema_version,
155
+ 'occurred_at' => @occurred_at.iso8601,
156
+ 'producer' => @producer,
157
+ 'trace_id' => @trace_id
158
+ }
159
+
160
+ if event_based?
161
+ envelope.merge!(
162
+ 'domain' => @domain,
163
+ 'resource' => @resource,
164
+ 'action' => @action,
165
+ 'resource_id' => @resource_id,
166
+ 'payload' => @payload
167
+ )
168
+ elsif topic_based?
169
+ envelope.merge!(
170
+ 'topic' => @topic,
171
+ 'message_type' => @message_type,
172
+ 'message' => @payload
173
+ )
174
+ else
175
+ envelope['payload'] = @payload
176
+ end
177
+
178
+ envelope.compact
179
+ end
180
+
181
+ # Convert to hash
182
+ #
183
+ # @return [Hash] Event as hash
184
+ def to_h
185
+ {
186
+ event_id: @event_id,
187
+ schema_version: @schema_version,
188
+ domain: @domain,
189
+ resource: @resource,
190
+ action: @action,
191
+ topic: @topic,
192
+ message_type: @message_type,
193
+ producer: @producer,
194
+ resource_id: @resource_id,
195
+ occurred_at: @occurred_at,
196
+ trace_id: @trace_id,
197
+ payload: @payload
198
+ }.compact
199
+ end
200
+
201
+ # String representation
202
+ #
203
+ # @return [String] Event description
204
+ def to_s
205
+ "Event(#{event_type}, id=#{@event_id})"
206
+ end
207
+
208
+ # Inspect representation
209
+ #
210
+ # @return [String] Detailed inspection
211
+ def inspect
212
+ "#<Event:#{event_type} id=#{@event_id} occurred_at=#{@occurred_at.iso8601}>"
213
+ end
214
+
215
+ # Equality comparison
216
+ #
217
+ # @param other [Object] Object to compare
218
+ # @return [Boolean] True if equal
219
+ def ==(other)
220
+ other.is_a?(Event) && @event_id == other.event_id
221
+ end
222
+
223
+ alias eql? ==
224
+
225
+ # Hash code for use in hash tables
226
+ #
227
+ # @return [Integer] Hash code
228
+ def hash
229
+ @event_id.hash
230
+ end
231
+
232
+ private
233
+
234
+ # Parse timestamp string to Time object
235
+ #
236
+ # @param timestamp [String, Time, nil] Timestamp
237
+ # @return [Time] Parsed time
238
+ def self.parse_timestamp(timestamp)
239
+ case timestamp
240
+ when Time
241
+ timestamp
242
+ when String
243
+ Time.parse(timestamp)
244
+ else
245
+ Time.now.utc
246
+ end
247
+ end
248
+
249
+ # Extract payload from envelope
250
+ #
251
+ # @param envelope [Hash] Event envelope
252
+ # @return [Hash] Payload data
253
+ def self.extract_payload(envelope)
254
+ # For event-based messages, payload is in 'payload'
255
+ # For topic-based messages, payload is in 'message'
256
+ envelope['payload'] || envelope[:payload] ||
257
+ envelope['message'] || envelope[:message] ||
258
+ {}
259
+ end
260
+
261
+ # Validate event data
262
+ #
263
+ # @raise [ArgumentError] if invalid
264
+ def validate!
265
+ raise ArgumentError, 'event_id is required' if @event_id.nil? || @event_id.empty?
266
+ raise ArgumentError, 'payload must be a Hash' unless @payload.is_a?(Hash)
267
+ raise ArgumentError, 'occurred_at must be a Time' unless @occurred_at.is_a?(Time)
268
+
269
+ # Must be either event-based or topic-based
270
+ unless event_based? || topic_based?
271
+ raise ArgumentError, 'Event must have either (domain, resource, action) or (topic)'
272
+ end
273
+ end
274
+ end
275
+ end