shared_broker 1.2.0 → 1.4.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: d47439f3383e9d40d13664b67dc8b77318b64f628e6a1efa0f4684130ff2ff4b
4
- data.tar.gz: 85d081f58ac1608a1629b29f14c634b370b963c65338eb7f9a32ace26bc90502
3
+ metadata.gz: b161a64893a9f15f9bb4568c1b281c2851904bc9e8730e8992d5f6d0847a4de4
4
+ data.tar.gz: 6a771b8df2dc371516b2d7f97d6e157ff493141034106c40236f952c6aef0f2e
5
5
  SHA512:
6
- metadata.gz: 25151e9182b26d17334f04b121831703eee6c8b95981ac2b235085472501f06ec97c2a6b14cedc434dd3ee49963577b8eceedc175680525025d31e3246ac81dd
7
- data.tar.gz: 07cb36e946dd24260fdd70d7cd995f4aeae8167b764d1df18e2026bc523481ec4d788308f720494472985a3b1a4adc7ae8751ded5abbd20b02c8c475c10a97c6
6
+ metadata.gz: '06494d11f1575ac5e13353acad3e9896f9ff7e5b7f64b164cecaa39b492aadeb170590e2ec9c9628c80691d08fc521e07e384fa43d2fe4e7874424704e39aedb'
7
+ data.tar.gz: 6781b513d75bd232c5c75738a6078aad08d096133e410335ec707ac203548b653af222bbb37ee7594fff40044e8122f8e362b12f68927939a7a19655f021ca16
data/CHANGELOG.md CHANGED
@@ -1,45 +1,60 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [1.2.0] - 2026-06-07
9
-
10
- ### Added
11
- - Pipeline of customizable Middlewares (Interceptors) for publishing and subscribing messages.
12
- - Distributed Tracing using W3C Trace Context (`traceparent`/`tracestate`) context injection and extraction.
13
-
14
- ## [1.1.1] - 2026-06-06
15
-
16
- ### Changed
17
- - Restructured `README.md` to clearly separate minimum required configuration from optional/advanced setups.
18
-
19
- ## [1.1.0] - 2026-06-06
20
-
21
- ### Added
22
- - Scalable Adapters:
23
- - Apache Kafka adapter (`SharedBroker::Adapters::Kafka`) with dynamic dependency loading.
24
- - Redis Pub/Sub adapter (`SharedBroker::Adapters::Redis`) with list-based DLQ routing.
25
- - Mock test configurations for Kafka and Redis to run adapter unit tests.
26
-
27
- ## [1.0.0] - 2026-06-06
28
-
29
- ### Added
30
- - Fault Tolerance & Resilience features:
31
- - Exponential backoff retry loop for message processing.
32
- - Automatic Dead Letter Queue (DLQ) routing with rich metadata headers (`x_failed_at`, `x_exception_class`, `x_exception_message`, `x_original_routing_key`).
33
- - Thread-safe `CircuitBreaker` wrapping all outbound publisher calls.
34
- - Schema Validation and Security:
35
- - Outbound/Inbound boundary validation using `dry-schema`.
36
- - Transparent AES-256-GCM symmetric payload encryption by default, configurable with `SharedBroker.encryption_key`.
37
- - Comprehensive test coverage for all the above features using isolated fakes and Minitest.
38
-
39
- ## [0.1.0] - 2026-06-06
40
-
41
- ### Added
42
- - Initial release with the pluggable `Client` messaging system.
43
- - `InMemory` adapter for local testing.
44
- - `RabbitMQ` adapter using the `bunny` gem.
45
- - Basic OpenTelemetry instrumentation utility (`SharedBroker::Telemetry`).
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.4.0] - 2026-06-10
9
+
10
+ ### Added
11
+ - Schema Registry Integration. Support for pluggable schema registries allowing dynamic schema validation during `publish` and `subscribe` actions.
12
+ - `Local` Schema Provider (default) leveraging `dry-schema` for backward compatibility.
13
+ - `Http` Schema Provider fetching JSON schemas over HTTP/HTTPS with in-memory caching and TTL.
14
+ - Validation using standard JSON Schema syntax for the `Http` provider.
15
+
16
+ ## [1.3.0] - 2026-06-08
17
+
18
+ ### Added
19
+ - Hybrid Multi-Adapter Routing to allow publishing and subscribing to different broker adapters (e.g., RabbitMQ, Kafka, Redis, or InMemory) dynamically.
20
+ - Support for exact match and wildcard matching (using File.fnmatch? glob patterns) routing rules, with default fallback adapter.
21
+ - Full backward compatibility with legacy single-adapter initialization.
22
+
23
+ ## [1.2.0] - 2026-06-07
24
+
25
+ ### Added
26
+ - Pipeline of customizable Middlewares (Interceptors) for publishing and subscribing messages.
27
+ - Distributed Tracing using W3C Trace Context (`traceparent`/`tracestate`) context injection and extraction.
28
+
29
+ ## [1.1.1] - 2026-06-06
30
+
31
+ ### Changed
32
+ - Restructured `README.md` to clearly separate minimum required configuration from optional/advanced setups.
33
+
34
+ ## [1.1.0] - 2026-06-06
35
+
36
+ ### Added
37
+ - Scalable Adapters:
38
+ - Apache Kafka adapter (`SharedBroker::Adapters::Kafka`) with dynamic dependency loading.
39
+ - Redis Pub/Sub adapter (`SharedBroker::Adapters::Redis`) with list-based DLQ routing.
40
+ - Mock test configurations for Kafka and Redis to run adapter unit tests.
41
+
42
+ ## [1.0.0] - 2026-06-06
43
+
44
+ ### Added
45
+ - Fault Tolerance & Resilience features:
46
+ - Exponential backoff retry loop for message processing.
47
+ - Automatic Dead Letter Queue (DLQ) routing with rich metadata headers (`x_failed_at`, `x_exception_class`, `x_exception_message`, `x_original_routing_key`).
48
+ - Thread-safe `CircuitBreaker` wrapping all outbound publisher calls.
49
+ - Schema Validation and Security:
50
+ - Outbound/Inbound boundary validation using `dry-schema`.
51
+ - Transparent AES-256-GCM symmetric payload encryption by default, configurable with `SharedBroker.encryption_key`.
52
+ - Comprehensive test coverage for all the above features using isolated fakes and Minitest.
53
+
54
+ ## [0.1.0] - 2026-06-06
55
+
56
+ ### Added
57
+ - Initial release with the pluggable `Client` messaging system.
58
+ - `InMemory` adapter for local testing.
59
+ - `RabbitMQ` adapter using the `bunny` gem.
60
+ - Basic OpenTelemetry instrumentation utility (`SharedBroker::Telemetry`).
data/README.md CHANGED
@@ -1,155 +1,204 @@
1
- # SharedBroker
2
-
3
- `SharedBroker` is a high-performance Ruby library designed to simplify event-based communication (asynchronous messaging) and telemetry (observability) in Rails microservice architectures.
4
-
5
- The library implements the **Adapter Pattern** to decouple your application from physical queue providers, allowing easy broker swapping and clean synchronous testing with an in-memory adapter.
6
-
7
- ---
8
-
9
- ## Key Features
10
-
11
- - **Pluggable Messaging**: Adapter pattern supporting:
12
- - `InMemory`: Synchronous local simulation for fast TDD testing (no inline external I/O stubs required).
13
- - `RabbitMQ`: Robust connection using the `bunny` gem.
14
- - `Kafka`: High-throughput adapter using the `kafka` gem.
15
- - `Redis`: Light-weight Pub/Sub broker using the `redis` gem.
16
- - **Resilience & Fault Tolerance**:
17
- - **Automatic Retry**: Automatic retry mechanism on message processing failures using exponential backoff.
18
- - **Dead Letter Queue (DLQ)**: Messages that exhaust their retries are automatically moved to a DLQ (`#{queue_name}.dlq` or a custom topic/list depending on the adapter) containing error metadata headers.
19
- - **Circuit Breaker**: Integrated thread-safe Circuit Breaker wrapping message publication to prevent cascading failures.
20
- - **Security & Data Validation**:
21
- - **Strict Schema Validation**: Integration with `dry-schema` to validate message structures on both publish (boundaries out) and subscribe (boundaries in).
22
- - **Transparent Payload Encryption**: Payloads are automatically encrypted at rest using AES-256-GCM via `SharedBroker.encryption_key`.
23
- - **Integrated OpenTelemetry**: Centralized SDK configuration with auto-instrumentation for all supported libraries (ActiveRecord, Bunny, Faraday, Rails, PG, etc.).
24
-
25
- ---
26
-
27
- ## Installation
28
-
29
- Add this line to your application's `Gemfile`:
30
-
31
- ```ruby
32
- gem "shared_broker", path: "gems/shared_broker" # for local gem
33
- # or when published:
34
- # gem "shared_broker"
35
- ```
36
-
37
- And execute:
38
-
39
- ```bash
40
- bundle install
41
- ```
42
-
43
- ---
44
-
45
- ## Configuration
46
-
47
- Create an initializer in your Rails application (`config/initializers/shared_broker.rb`). Below is the breakdown of what is **required** versus what is **optional**.
48
-
49
- ### 1. Required Configuration (Minimum Setup)
50
-
51
- You must configure the adapter depending on the environment, initialize the client, and configure the payload encryption key (required since AES-256-GCM is active by default):
52
-
53
- ```ruby
54
- require "shared_broker"
55
-
56
- # A. Configure Payload Encryption Key (AES-256-GCM)
57
- # Expects a 32-byte string. Use a secure production key in production.
58
- SharedBroker.encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
59
-
60
- # B. Configure the Adapter based on Environment
61
- if Rails.env.test?
62
- # In-memory adapter prevents external queue dependency during unit tests
63
- BROKER_ADAPTER = SharedBroker::Adapters::InMemory.new
64
- else
65
- # Connects to real RabbitMQ broker
66
- amqp_url = ENV.fetch("RABBITMQ_URL") { "amqp://guest:guest@localhost:5672" }
67
- BROKER_ADAPTER = SharedBroker::Adapters::RabbitMQ.new(amqp_url: amqp_url)
68
- end
69
-
70
- # C. Instantiate the Client by Injecting the Adapter
71
- SPOT_BROKER = SharedBroker::Client.new(adapter: BROKER_ADAPTER)
72
- ```
73
-
74
- ---
75
-
76
- ### 2. Optional Configuration
77
-
78
- These features can be configured optionally depending on your needs.
79
-
80
- #### A. Event Payload Validation (dry-schema)
81
- Register schemas to validate payload structure automatically on outbound (`publish`) and inbound (`subscribe`) boundaries:
82
-
83
- ```ruby
84
- user_created_schema = Dry::Schema.Params do
85
- required(:id).filled(:integer)
86
- required(:email).filled(:string)
87
- end
88
-
89
- SharedBroker::Validation.register("user.created", user_created_schema)
90
- ```
91
-
92
- #### B. Custom Circuit Breaker
93
- By default, the client instantiates a standard Circuit Breaker. You can provide a custom one to tune the failure threshold and recovery window:
94
-
95
- ```ruby
96
- custom_circuit_breaker = SharedBroker::CircuitBreaker.new(
97
- failure_threshold: 5, # trip circuit after 5 failures
98
- recovery_timeout: 30 # wait 30 seconds before attempting recovery
99
- )
100
-
101
- SPOT_BROKER = SharedBroker::Client.new(
102
- adapter: BROKER_ADAPTER,
103
- circuit_breaker: custom_circuit_breaker
104
- )
105
- ```
106
-
107
- #### C. Initialize Distributed Tracing (OpenTelemetry)
108
- Initialize the OpenTelemetry SDK with auto-instrumentation for the microservice:
109
-
110
- ```ruby
111
- SharedBroker::Telemetry.configure(service_name: "my_microservice")
112
- ```
113
-
114
- ---
115
-
116
- ## Usage
117
-
118
- ### Publishing Events
119
- Send events by passing the topic name and a structured payload (must be a `Hash`):
120
-
121
- ```ruby
122
- event_data = {
123
- id: 1,
124
- email: "test@example.com"
125
- }
126
-
127
- # The payload will be validated against its dry-schema, encrypted, and published safely.
128
- SPOT_BROKER.publish("user.created", event_data)
129
- ```
130
-
131
- ### Subscribing to Events (Consumer with Retry and DLQ)
132
- To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries and backoff rate:
133
-
134
- ```ruby
135
- SPOT_BROKER.subscribe("user.created", "my_consumption_queue", max_retries: 3, backoff_base: 2) do |payload|
136
- puts "Decrypted event successfully validated & consumed! ID: #{payload[:id]}"
137
- # execute your business logic here...
138
- end
139
- ```
140
-
141
- ---
142
-
143
- ## Running Gem Tests
144
-
145
- To run the unit test suite using **Minitest**:
146
-
147
- ```bash
148
- bundle exec rake test
149
- ```
150
-
151
- ---
152
-
153
- ## License
154
-
155
- This Gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1
+ [![Gem Version](https://badge.fury.io/rb/shared_broker.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/shared_broker)
2
+
3
+ # SharedBroker
4
+
5
+ `SharedBroker` is a high-performance Ruby library designed to simplify event-based communication (asynchronous messaging) and telemetry (observability) in Rails microservice architectures.
6
+
7
+ The library implements the **Adapter Pattern** to decouple your application from physical queue providers, allowing easy broker swapping and clean synchronous testing with an in-memory adapter.
8
+
9
+ ---
10
+
11
+ ## Key Features
12
+
13
+ - **Pluggable Messaging**: Adapter pattern supporting:
14
+ - `InMemory`: Synchronous local simulation for fast TDD testing (no inline external I/O stubs required).
15
+ - `RabbitMQ`: Robust connection using the `bunny` gem.
16
+ - `Kafka`: High-throughput adapter using the `kafka` gem.
17
+ - `Redis`: Light-weight Pub/Sub broker using the `redis` gem.
18
+ - **Resilience & Fault Tolerance**:
19
+ - **Automatic Retry**: Automatic retry mechanism on message processing failures using exponential backoff.
20
+ - **Dead Letter Queue (DLQ)**: Messages that exhaust their retries are automatically moved to a DLQ (`#{queue_name}.dlq` or a custom topic/list depending on the adapter) containing error metadata headers.
21
+ - **Circuit Breaker**: Integrated thread-safe Circuit Breaker wrapping message publication to prevent cascading failures.
22
+ - **Security & Data Validation**:
23
+ - **Strict Schema Validation**: Integration with `dry-schema` to validate message structures on both publish (boundaries out) and subscribe (boundaries in).
24
+ - **Transparent Payload Encryption**: Payloads are automatically encrypted at rest using AES-256-GCM via `SharedBroker.encryption_key`.
25
+ - **Integrated OpenTelemetry**: Centralized SDK configuration with auto-instrumentation for all supported libraries (ActiveRecord, Bunny, Faraday, Rails, PG, etc.).
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's `Gemfile`:
32
+
33
+ ```ruby
34
+ gem "shared_broker", path: "gems/shared_broker" # for local gem
35
+ # or when published:
36
+ # gem "shared_broker"
37
+ ```
38
+
39
+ And execute:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Configuration
48
+
49
+ Create an initializer in your Rails application (`config/initializers/shared_broker.rb`). Below is the breakdown of what is **required** versus what is **optional**.
50
+
51
+ ### 1. Required Configuration (Minimum Setup)
52
+
53
+ You must configure the adapter depending on the environment, initialize the client, and configure the payload encryption key (required since AES-256-GCM is active by default):
54
+
55
+ ```ruby
56
+ require "shared_broker"
57
+
58
+ # A. Configure Payload Encryption Key (AES-256-GCM)
59
+ # Expects a 32-byte string. Use a secure production key in production.
60
+ SharedBroker.encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
61
+
62
+ # B. Configure the Adapter based on Environment
63
+ if Rails.env.test?
64
+ # In-memory adapter prevents external queue dependency during unit tests
65
+ BROKER_ADAPTER = SharedBroker::Adapters::InMemory.new
66
+ else
67
+ # Connects to real RabbitMQ broker
68
+ amqp_url = ENV.fetch("RABBITMQ_URL") { "amqp://guest:guest@localhost:5672" }
69
+ BROKER_ADAPTER = SharedBroker::Adapters::RabbitMQ.new(amqp_url: amqp_url)
70
+ end
71
+
72
+ # C. Instantiate the Client by Injecting the Adapter
73
+ SPOT_BROKER = SharedBroker::Client.new(adapter: BROKER_ADAPTER)
74
+ ```
75
+
76
+ ---
77
+
78
+ ### 2. Optional Configuration
79
+
80
+ These features can be configured optionally depending on your needs.
81
+
82
+ #### A. Event Payload Validation & Schema Registry
83
+
84
+ `SharedBroker` supports validation on outbound (`publish`) and inbound (`subscribe`) boundaries. It includes a pluggable **Schema Registry** supporting both local definitions and remote registry servers.
85
+
86
+ ##### A1. Local Validation (dry-schema - default)
87
+ Register schemas to validate payload structure locally using `dry-schema`:
88
+
89
+ ```ruby
90
+ user_created_schema = Dry::Schema.Params do
91
+ required(:id).filled(:integer)
92
+ required(:email).filled(:string)
93
+ end
94
+
95
+ # Registered on the default local provider
96
+ SharedBroker::Validation.register("user.created", user_created_schema)
97
+ ```
98
+
99
+ ##### A2. Http Schema Registry (JSON Schema)
100
+ Configure `SharedBroker` to fetch schemas dynamically from an HTTP-based Schema Registry. The HTTP provider validates payloads using the standard JSON Schema specification and caches schemas in-memory to prevent validation latency:
101
+
102
+ ```ruby
103
+ # Configure the HTTP Schema Registry provider
104
+ SharedBroker::SchemaRegistry.provider = SharedBroker::SchemaRegistry::Providers::Http.new(
105
+ url: "https://schema-registry.mycorp.internal",
106
+ headers: { "Authorization" => "Bearer my-secret-token" },
107
+ cache_ttl: 300 # cache schemas in-memory for 5 minutes
108
+ )
109
+ ```
110
+ When configured with the HTTP provider, any published or subscribed event will automatically trigger a lookup against `https://schema-registry.mycorp.internal/schemas/{topic}.json`.
111
+
112
+
113
+ #### B. Custom Circuit Breaker
114
+ By default, the client instantiates a standard Circuit Breaker. You can provide a custom one to tune the failure threshold and recovery window:
115
+
116
+ ```ruby
117
+ custom_circuit_breaker = SharedBroker::CircuitBreaker.new(
118
+ failure_threshold: 5, # trip circuit after 5 failures
119
+ recovery_timeout: 30 # wait 30 seconds before attempting recovery
120
+ )
121
+
122
+ SPOT_BROKER = SharedBroker::Client.new(
123
+ adapter: BROKER_ADAPTER,
124
+ circuit_breaker: custom_circuit_breaker
125
+ )
126
+ ```
127
+
128
+ #### C. Initialize Distributed Tracing (OpenTelemetry)
129
+ Initialize the OpenTelemetry SDK with auto-instrumentation for the microservice:
130
+
131
+ ```ruby
132
+ SharedBroker::Telemetry.configure(service_name: "my_microservice")
133
+ ```
134
+
135
+ #### D. Hybrid Multi-Adapter Routing
136
+ If your system requires directing different topics to different message brokers (e.g., Kafka for telemetry, RabbitMQ for transactional events, Redis for quick cache invalidation), you can configure `SharedBroker::Client` with multiple adapters and a routing table:
137
+
138
+ ```ruby
139
+ # 1. Initialize physical adapters
140
+ rabbit_adapter = SharedBroker::Adapters::RabbitMQ.new(amqp_url: "amqp://guest:guest@localhost:5672")
141
+ kafka_adapter = SharedBroker::Adapters::Kafka.new(seed_brokers: ["localhost:9092"])
142
+ redis_adapter = SharedBroker::Adapters::Redis.new(redis_url: "redis://localhost:6379")
143
+
144
+ # 2. Instantiate the Client in Hybrid mode
145
+ HYBRID_BROKER = SharedBroker::Client.new(
146
+ adapters: {
147
+ rabbitmq: rabbit_adapter,
148
+ kafka: kafka_adapter,
149
+ redis: redis_adapter
150
+ },
151
+ routing: {
152
+ # Exact routing match
153
+ "payment.processed" => :rabbitmq,
154
+ # Wildcard routing match
155
+ "telemetry.*" => :kafka,
156
+ "cache.*" => :redis,
157
+ # Default fallback routing
158
+ "*" => :rabbitmq
159
+ }
160
+ )
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Usage
166
+
167
+ ### Publishing Events
168
+ Send events by passing the topic name and a structured payload (must be a `Hash`):
169
+
170
+ ```ruby
171
+ event_data = {
172
+ id: 1,
173
+ email: "test@example.com"
174
+ }
175
+
176
+ # The payload will be validated against its dry-schema, encrypted, and published safely.
177
+ SPOT_BROKER.publish("user.created", event_data)
178
+ ```
179
+
180
+ ### Subscribing to Events (Consumer with Retry and DLQ)
181
+ To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries and backoff rate:
182
+
183
+ ```ruby
184
+ SPOT_BROKER.subscribe("user.created", "my_consumption_queue", max_retries: 3, backoff_base: 2) do |payload|
185
+ puts "Decrypted event successfully validated & consumed! ID: #{payload[:id]}"
186
+ # execute your business logic here...
187
+ end
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Running Gem Tests
193
+
194
+ To run the unit test suite using **Minitest**:
195
+
196
+ ```bash
197
+ bundle exec rake test
198
+ ```
199
+
200
+ ---
201
+
202
+ ## License
203
+
204
+ This Gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "json-schema"
6
+
7
+ module SharedBroker
8
+ module SchemaRegistry
9
+ module Providers
10
+ class Http
11
+ def initialize(url:, headers: {}, cache_ttl: 300)
12
+ @base_url = url.chomp("/")
13
+ @headers = headers
14
+ @cache_ttl = cache_ttl
15
+ @cache = {}
16
+ end
17
+
18
+ def validate!(topic, payload)
19
+ schema = fetch_schema(topic)
20
+ return unless schema
21
+
22
+ begin
23
+ JSON::Validator.validate!(schema, payload)
24
+ rescue JSON::Schema::ValidationError => e
25
+ raise SharedBroker::Validation::ValidationError,
26
+ "Schema validation failed for topic #{topic.inspect} against schema #{schema.inspect}. Offending payload: #{payload.inspect}. Error: #{e.message}"
27
+ end
28
+ end
29
+
30
+ def clear_cache
31
+ @cache.clear
32
+ end
33
+
34
+ private
35
+
36
+ def fetch_schema(topic)
37
+ cached = @cache[topic.to_s]
38
+ return cached[:schema] if cached && cached[:expires_at] > Time.now
39
+
40
+ schema = download_schema(topic)
41
+ @cache[topic.to_s] = { schema: schema, expires_at: Time.now + @cache_ttl } if schema
42
+ schema
43
+ end
44
+
45
+ def download_schema(topic)
46
+ uri = URI("#{@base_url}/schemas/#{topic}.json")
47
+ request = Net::HTTP::Get.new(uri)
48
+ @headers.each { |k, v| request[k.to_s] = v.to_s }
49
+
50
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
51
+ http.request(request)
52
+ end
53
+
54
+ return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
55
+
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-schema"
4
+
5
+ module SharedBroker
6
+ module SchemaRegistry
7
+ module Providers
8
+ class Local
9
+ def initialize
10
+ @schemas = {}
11
+ end
12
+
13
+ def register(topic, schema)
14
+ unless schema.respond_to?(:call)
15
+ raise ArgumentError, "Expected schema to respond to :call, got #{schema.class} with value #{schema.inspect}. Expected shape: respond_to?(:call)"
16
+ end
17
+
18
+ @schemas[topic.to_s] = schema
19
+ end
20
+
21
+ def validate!(topic, payload)
22
+ schema = @schemas[topic.to_s]
23
+ return unless schema
24
+
25
+ result = schema.call(payload)
26
+ return if result.success?
27
+
28
+ raise SharedBroker::Validation::ValidationError,
29
+ "Schema validation failed for topic #{topic.inspect}. Expected keys: #{schema.rules.keys.inspect}, got payload: #{payload.inspect}. Errors: #{result.errors.to_h.inspect}"
30
+ end
31
+
32
+ def clear
33
+ @schemas.clear
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedBroker
4
+ module SchemaRegistry
5
+ class << self
6
+ attr_accessor :provider
7
+ end
8
+
9
+ def self.validate!(topic, payload)
10
+ resolved_provider = provider || default_provider
11
+ resolved_provider.validate!(topic, payload)
12
+ end
13
+
14
+ def self.clear_cache
15
+ return unless provider.respond_to?(:clear_cache)
16
+
17
+ provider.clear_cache
18
+ end
19
+
20
+ def self.default_provider
21
+ @default_provider ||= SharedBroker::SchemaRegistry::Providers::Local.new
22
+ end
23
+ private_class_method :default_provider
24
+ end
25
+ end
@@ -1,32 +1,27 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry-schema"
4
-
5
- module SharedBroker
6
- module Validation
7
- class ValidationError < StandardError; end
8
-
9
- @schemas = {}
10
-
11
- def self.register(topic, schema)
12
- unless schema.respond_to?(:call)
13
- raise ArgumentError, "Expected schema to respond to :call, got #{schema.class} with value #{schema.inspect}"
14
- end
15
- @schemas[topic.to_s] = schema
16
- end
17
-
18
- def self.validate!(topic, message)
19
- schema = @schemas[topic.to_s]
20
- return unless schema
21
-
22
- result = schema.call(message)
23
- unless result.success?
24
- raise ValidationError, "Schema validation failed for topic #{topic.inspect}. Expected keys: #{schema.rules.keys.inspect}, got payload: #{message.inspect}. Errors: #{result.errors.to_h.inspect}"
25
- end
26
- end
27
-
28
- def self.clear
29
- @schemas.clear
30
- end
31
- end
32
- end
1
+ # frozen_string_literal: true
2
+
3
+ module SharedBroker
4
+ module Validation
5
+ class ValidationError < StandardError; end
6
+
7
+ def self.register(topic, schema)
8
+ provider = SharedBroker::SchemaRegistry.provider || SharedBroker::SchemaRegistry.send(:default_provider)
9
+ if provider.respond_to?(:register)
10
+ provider.register(topic, schema)
11
+ else
12
+ raise RuntimeError, "Current SchemaRegistry provider #{provider.class} does not support local registration. Expected a provider that responds to :register."
13
+ end
14
+ end
15
+
16
+ def self.validate!(topic, message)
17
+ SharedBroker::SchemaRegistry.validate!(topic, message)
18
+ end
19
+
20
+ def self.clear
21
+ SharedBroker::SchemaRegistry.clear_cache
22
+ provider = SharedBroker::SchemaRegistry.provider || SharedBroker::SchemaRegistry.send(:default_provider)
23
+ provider.clear if provider.respond_to?(:clear)
24
+ end
25
+ end
26
+ end
27
+
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module SharedBroker
4
- VERSION = "1.2.0"
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module SharedBroker
4
+ VERSION = "1.4.0"
5
+ end
data/lib/shared_broker.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  require_relative "shared_broker/version"
4
4
  require_relative "shared_broker/telemetry"
5
5
  require_relative "shared_broker/circuit_breaker"
6
+ require_relative "shared_broker/schema_registry"
7
+ require_relative "shared_broker/schema_registry/providers/local"
8
+ require_relative "shared_broker/schema_registry/providers/http"
6
9
  require_relative "shared_broker/validation"
7
10
  require_relative "shared_broker/cipher"
8
11
  require_relative "shared_broker/middleware_pipeline"
@@ -22,16 +25,11 @@ module SharedBroker
22
25
  @encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
23
26
 
24
27
  class Client
25
- attr_reader :circuit_breaker, :middleware_pipeline
28
+ attr_reader :circuit_breaker, :middleware_pipeline, :adapters, :routing
26
29
 
27
- def initialize(adapter:, circuit_breaker: nil, middlewares: nil)
28
- unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
29
- raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got #{adapter.class} with value #{adapter.inspect}"
30
- end
31
-
32
- @adapter = adapter
30
+ def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil)
31
+ setup_adapters(adapter: adapter, adapters: adapters, routing: routing)
33
32
  @circuit_breaker = circuit_breaker || CircuitBreaker.new
34
-
35
33
  resolved_middlewares = middlewares || [SharedBroker::Middlewares::OpenTelemetryPropagation.new]
36
34
  @middleware_pipeline = MiddlewarePipeline.new(resolved_middlewares)
37
35
  end
@@ -43,13 +41,13 @@ module SharedBroker
43
41
  encrypted_msg = SharedBroker::Cipher.encrypt(message, SharedBroker.encryption_key)
44
42
 
45
43
  @circuit_breaker.run do
46
- @adapter.publish(topic, encrypted_msg, correlation_id: correlation_id)
44
+ resolve_adapter(topic).publish(topic, encrypted_msg, correlation_id: correlation_id)
47
45
  end
48
46
  end
49
47
  end
50
48
 
51
49
  def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
52
- @adapter.subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
50
+ resolve_adapter(topic).subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
53
51
  decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, SharedBroker.encryption_key)
54
52
  SharedBroker::Validation.validate!(topic, decrypted_msg)
55
53
 
@@ -59,5 +57,55 @@ module SharedBroker
59
57
  end
60
58
  end
61
59
  end
60
+
61
+ private
62
+
63
+ def setup_adapters(adapter: nil, adapters: nil, routing: nil)
64
+ if adapter || (adapters.nil? && routing.nil?)
65
+ validate_single_adapter!(adapter)
66
+ @adapters = { default: adapter }
67
+ @routing = { "*" => :default }
68
+ else
69
+ validate_multi_adapters!(adapters, routing)
70
+ @adapters = adapters
71
+ @routing = routing.transform_keys(&:to_s)
72
+ end
73
+ end
74
+
75
+ def validate_single_adapter!(adapter)
76
+ unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
77
+ raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got: #{adapter.inspect} (must shape like SharedBroker::Adapters::Base)"
78
+ end
79
+ end
80
+
81
+ def validate_multi_adapters!(adapters, routing)
82
+ unless adapters.is_a?(Hash) && routing.is_a?(Hash)
83
+ raise ArgumentError, "Expected adapters and routing to be Hashes, got adapters: #{adapters.inspect} (class: #{adapters.class}), routing: #{routing.inspect} (class: #{routing.class})"
84
+ end
85
+ adapters.each do |key, ad|
86
+ unless ad.respond_to?(:publish) && ad.respond_to?(:subscribe)
87
+ raise ArgumentError, "Expected adapter #{key.inspect} to respond to :publish and :subscribe, got: #{ad.inspect} (class: #{ad.class})"
88
+ end
89
+ end
90
+ end
91
+
92
+ def resolve_adapter(topic)
93
+ topic_str = topic.to_s
94
+ return @adapters[@routing[topic_str]] if @routing.key?(topic_str)
95
+
96
+ @routing.each do |pattern, adapter_key|
97
+ next if pattern == "*"
98
+ if File.fnmatch?(pattern, topic_str)
99
+ return @adapters[adapter_key]
100
+ end
101
+ end
102
+
103
+ fallback_key = @routing["*"]
104
+ if fallback_key && @adapters.key?(fallback_key)
105
+ @adapters[fallback_key]
106
+ else
107
+ raise RuntimeError, "No adapter resolved for topic: #{topic.inspect}. Expected one of #{@routing.keys.inspect}"
108
+ end
109
+ end
62
110
  end
63
111
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shared_broker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wesley Lima
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-07 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json-schema
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: opentelemetry-api
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +157,9 @@ files:
143
157
  - lib/shared_broker/circuit_breaker.rb
144
158
  - lib/shared_broker/middleware_pipeline.rb
145
159
  - lib/shared_broker/middlewares/open_telemetry_propagation.rb
160
+ - lib/shared_broker/schema_registry.rb
161
+ - lib/shared_broker/schema_registry/providers/http.rb
162
+ - lib/shared_broker/schema_registry/providers/local.rb
146
163
  - lib/shared_broker/telemetry.rb
147
164
  - lib/shared_broker/validation.rb
148
165
  - lib/shared_broker/version.rb