leopard 0.1.5 → 0.1.7
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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/.version.txt +1 -1
- data/CHANGELOG.md +15 -0
- data/doc/service-api-vs-rest.adoc +149 -0
- data/examples/echo_endpoint.rb +11 -2
- data/lib/leopard/message_wrapper.rb +2 -3
- data/lib/leopard/nats_api_server.rb +149 -41
- data/lib/leopard/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 343e274b8fffbe093d59b1d819a4b0be0ba9efe109c4883c535ba3692f1854a3
|
4
|
+
data.tar.gz: 6b32780cf1bd40d7b0191a79a533382dbc6ed828162d9c6ebeefa2c838191c77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ee58b9e7a03d9dee24f3d338e700c0d0456ff0056379e06405418365023a14dc7484bad768d7deff2727cb7e01f0daa331234bfa6b0875b5891f79577a1880e
|
7
|
+
data.tar.gz: '047850ac66d9a789bdaad311ef764bed2bea365776a67d6f47f9634eb039edc303ab48d6227ad443f4f2a49b5fba1f1cd63c5d45068120b544d9a1e8a65c292d'
|
data/.version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.1.7](https://github.com/rubyists/leopard/compare/v0.1.6...v0.1.7) (2025-08-06)
|
4
|
+
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
* Allow non blocking all the way down to the instance level ([#27](https://github.com/rubyists/leopard/issues/27)) ([01748a5](https://github.com/rubyists/leopard/commit/01748a56bc927ee1dbc70d2351fd12037e5b4bef))
|
9
|
+
* Stop sending code argument to respond_with_error, it does not accept it ([#23](https://github.com/rubyists/leopard/issues/23)) ([9d87b8c](https://github.com/rubyists/leopard/commit/9d87b8c308a1fdff72769863711bb6bb942b3677))
|
10
|
+
|
11
|
+
## [0.1.6](https://github.com/rubyists/leopard/compare/v0.1.5...v0.1.6) (2025-08-03)
|
12
|
+
|
13
|
+
|
14
|
+
### Features
|
15
|
+
|
16
|
+
* Adds graceful shutdown when INT/TERM/QUIT signal is received ([#18](https://github.com/rubyists/leopard/issues/18)) ([ce03fb0](https://github.com/rubyists/leopard/commit/ce03fb00afcbbadadc413766b62df9451f7b73b8))
|
17
|
+
|
3
18
|
## [0.1.5](https://github.com/rubyists/leopard/compare/v0.1.4...v0.1.5) (2025-07-31)
|
4
19
|
|
5
20
|
|
@@ -0,0 +1,149 @@
|
|
1
|
+
= NATS ServiceApi (via Leopard) vs REST
|
2
|
+
:revdate: 2025-08-03
|
3
|
+
:doctype: whitepaper
|
4
|
+
|
5
|
+
== Abstract
|
6
|
+
Leopard’s NATS ServiceApi wrapper delivers full-featured service-to-service communication, comparable with REST/GRPC.
|
7
|
+
With NATS, you get discovery, health checks, and observability, all at the protocol layer.
|
8
|
+
|
9
|
+
All Leopard does is add an Easy Button to serve these endpoints with concurrency to scale NATS connections
|
10
|
+
horizontally, and to allow for per-endpoint (or groups of endpoints) scaling.
|
11
|
+
|
12
|
+
This abstraction preserves the simplicity of REST requests with the resilience of asynchronous messaging
|
13
|
+
under the hood.
|
14
|
+
|
15
|
+
This paper contrasts the NATS ServiceApi model with traditional REST, outlines its key benefits and trade-offs,
|
16
|
+
and illustrates how Leopard can specifically streamline microservice (and nanoservice) architectures.
|
17
|
+
|
18
|
+
== 1. Introduction
|
19
|
+
Microservices communicate most often via HTTP/REST today, but REST comes with overhead: service registries,
|
20
|
+
load-balancers, schema/version endpoints, and brittle synchronous request patterns.
|
21
|
+
Leopard leverages NATS’s ServiceApi layer to deliver:
|
22
|
+
|
23
|
+
* **Automatic discovery** via `$SRV.PING/INFO/STATS` subjects
|
24
|
+
* **Dynamic load-balanced routing** with queue groups
|
25
|
+
* **Built-in telemetry** (per-endpoint request counts, latencies, errors)
|
26
|
+
* **Scalable workers** (Concurrent::FixedThreadPool with Leopard, currently)
|
27
|
+
* **Versioned endpoints** multiple versions can be registered concurrently, no need for /v1, /v2, etc.
|
28
|
+
|
29
|
+
== 2. Architecture Overview
|
30
|
+
Leopard embeds a “mini web-framework” in Ruby, registering each endpoint on a NATS subject and grouping instances for load-sharing:
|
31
|
+
|
32
|
+
[source,mermaid]
|
33
|
+
----
|
34
|
+
graph LR
|
35
|
+
subgraph ServiceInstance
|
36
|
+
A[NatsApiServer.run] --> B[Registers endpoints]
|
37
|
+
end
|
38
|
+
subgraph NATS_Cluster
|
39
|
+
B --> C["$SRV.INFO.calc"]
|
40
|
+
B --> D["calc.add queue group"]
|
41
|
+
end
|
42
|
+
E[Client] -->|request calc.add| D
|
43
|
+
D --> F[Worker Thread pool]
|
44
|
+
F --> G[Handler & Dry Monads]
|
45
|
+
G -->|respond or error| E
|
46
|
+
----
|
47
|
+
|
48
|
+
== 3. Key Benefits
|
49
|
+
|
50
|
+
=== 3.1 Automatic Discovery & Monitoring
|
51
|
+
Leopard services auto-advertise on well-known NATS subjects. Clients can query:
|
52
|
+
|
53
|
+
* `$SRV.PING.<name>` – discover live instances & measure RTT
|
54
|
+
* `$SRV.INFO.<name>` – retrieve endpoint schemas & metadata
|
55
|
+
* `$SRV.STATS.<name>` – fetch per-endpoint metrics
|
56
|
+
|
57
|
+
No external service-registry (Consul, etcd) or custom HTTP health paths required.
|
58
|
+
|
59
|
+
=== 3.2 Scaling-Per-Endpoint-Group
|
60
|
+
Each endpoint (mapped to a NATS subject) is registered with an optional queue group.
|
61
|
+
This allows it to enjoy native NATS queue-group load balancing. You can:
|
62
|
+
|
63
|
+
* Scale thread-pooled workers independently per service
|
64
|
+
* Horizontally add new service instances without redeploying clients
|
65
|
+
* Isolate hot-paths (e.g. “reports.generate”) onto dedicated worker farms
|
66
|
+
|
67
|
+
=== 3.3 Observability & Telemetry
|
68
|
+
Leopard exposes stats out-of-the-box:
|
69
|
+
|
70
|
+
* Request counts, error counts, processing time
|
71
|
+
* Custom `on_stats` hooks for business metrics
|
72
|
+
* Integration with Prometheus or any NATS-capable dashboard
|
73
|
+
|
74
|
+
=== 3.4 Asynchronous, Resilient Communication
|
75
|
+
Unlike blocking HTTP calls, Leopard’s NATS requests can:
|
76
|
+
|
77
|
+
* Employ timeouts, retries, and dead-letter queues
|
78
|
+
* Fit into event-driven pipelines, decoupling producers and consumers
|
79
|
+
* Maintain throughput under partial outages
|
80
|
+
|
81
|
+
== 4. Comparison with REST
|
82
|
+
[cols="1,1,1", options="header"]
|
83
|
+
|===
|
84
|
+
| Feature | REST (HTTP) | NATS ServiceApi
|
85
|
+
|
86
|
+
| Discovery
|
87
|
+
| Requires external service registry or API gateway, A.K.A. "it's always DNS"
|
88
|
+
| Built-in via `$SRV.PING`, `$SRV.INFO`, `$SRV.STATS` Works uniformly across all languages
|
89
|
+
|
90
|
+
| Load Balancing
|
91
|
+
| HTTP load balancer or DNS round-robin
|
92
|
+
| Native queue-group load balancing per subject
|
93
|
+
|
94
|
+
| Telemetry
|
95
|
+
| Custom instrumentation (e.g., `/metrics` endpoint)
|
96
|
+
| Auto-collected stats (`service.stats`) and `on_stats` hooks
|
97
|
+
|
98
|
+
| Latency & Overhead
|
99
|
+
| Higher (HTTP/TCP handshake, headers, JSON)
|
100
|
+
| Low-latency binary protocol with optional JSON payloads (other formats supported with plugins)
|
101
|
+
|
102
|
+
| Communication Model
|
103
|
+
| Synchronous, blocking request/response
|
104
|
+
| Asynchronous request/reply, decoupled via subjects
|
105
|
+
|
106
|
+
| Schema & Validation
|
107
|
+
| OpenAPI/Swagger externally managed
|
108
|
+
| Optional metadata on endpoints + pluggable middleware
|
109
|
+
|
110
|
+
| Error Handling
|
111
|
+
| HTTP status codes and response bodies
|
112
|
+
| Standardized error headers (`Nats-Service-Error`, `Nats-Service-Error-Code`)
|
113
|
+
|
114
|
+
| Multi-Language Support
|
115
|
+
| Varies by framework; patterns differ per language
|
116
|
+
| Uniform ServiceApi semantics with native clients in all major languages
|
117
|
+
|
118
|
+
| Scalability
|
119
|
+
| Scale replicas behind LB
|
120
|
+
| Scale thread pools vertically + horizontal instances independently, by endpoint groups (or even a single endpoint)
|
121
|
+
|===
|
122
|
+
|
123
|
+
== 5. Trade-Offs & Considerations
|
124
|
+
. **Dependency on NATS**
|
125
|
+
Leopard requires a healthy NATS cluster; network partition or broker outage impacts all services. (This is not unlike Redis or Postgres dependencies)
|
126
|
+
. **Learning Curve**
|
127
|
+
Teams must understand NATS subjects, queue groups, and ServiceApi conventions. (Easier with helpers like Leopard’s `NatsApiServer`.)
|
128
|
+
. **Language Support**
|
129
|
+
While Leopard is Ruby-centric, NATS ServiceApi is cross-language. All teams must adopt compatible clients. (And handle concurrency and error handling in their own way.)
|
130
|
+
. **Subject Naming**
|
131
|
+
Adopting a consistent naming convention for subjects is crucial. This can be a challenge in large teams.
|
132
|
+
NATS can support a massive number of subjects. But to avoid confusion, subjects should have
|
133
|
+
clear, descriptive names that reflect the service and endpoint purpose.
|
134
|
+
Even though NATS service API exposes auto-discovery, there could (should?) be a central authoritative
|
135
|
+
document that defines the subject structure and naming conventions. This can avoid the TOCTOE issue.
|
136
|
+
(You check for a subject, it's not used, but then someone implements it in the meantime.)
|
137
|
+
That leads to: there should be a "registry" of subjects, that can be queried by developers
|
138
|
+
to discover available subjects. This can avoid confusion and ensure that all developers are on the same
|
139
|
+
page and not conflicting with one another.
|
140
|
+
|
141
|
+
== 6. What, then?
|
142
|
+
Leopard’s NATS ServiceApi framework offers a powerful alternative to REST:
|
143
|
+
zero-config discovery, per-endpoint scaling, rich observability, and asynchronous resilience.
|
144
|
+
|
145
|
+
For high-throughput, low-latency microservice (nano-service?) ecosystems, Leopard can simplify infrastructure,
|
146
|
+
reduce boilerplate, and improve operational visibility.
|
147
|
+
|
148
|
+
Leopard's aim is to retain the expressiveness and composability of idiomatic Ruby, while leveraging
|
149
|
+
NATS's ServiceApi performance and flexibility.
|
data/examples/echo_endpoint.rb
CHANGED
@@ -7,13 +7,22 @@ require_relative '../lib/leopard/nats_api_server'
|
|
7
7
|
class EchoService
|
8
8
|
include Rubyists::Leopard::NatsApiServer
|
9
9
|
|
10
|
+
def initialize(a_var = 1)
|
11
|
+
logger.info "EchoService initialized with a_var: #{a_var}"
|
12
|
+
end
|
13
|
+
|
10
14
|
endpoint(:echo) { |msg| Success(msg.data) }
|
15
|
+
endpoint(:echo_fail) { |msg| Failure({ failure: '*boom*', data: msg.data }) }
|
11
16
|
end
|
12
17
|
|
13
18
|
if __FILE__ == $PROGRAM_NAME
|
14
19
|
EchoService.run(
|
15
20
|
nats_url: 'nats://localhost:4222',
|
16
|
-
service_opts: {
|
17
|
-
|
21
|
+
service_opts: {
|
22
|
+
name: 'example.echo',
|
23
|
+
version: '1.0.0',
|
24
|
+
instance_args: [2],
|
25
|
+
},
|
26
|
+
instances: 1,
|
18
27
|
)
|
19
28
|
end
|
@@ -30,11 +30,10 @@ module Rubyists
|
|
30
30
|
end
|
31
31
|
|
32
32
|
# @param err [String, Exception] The error message or exception to respond with.
|
33
|
-
# @param code [Integer] The HTTP status code to use for the error response.
|
34
33
|
#
|
35
34
|
# @return [void]
|
36
|
-
def respond_with_error(err
|
37
|
-
raw.respond_with_error(err.to_s
|
35
|
+
def respond_with_error(err)
|
36
|
+
raw.respond_with_error(err.to_s)
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'nats/client'
|
4
4
|
require 'dry/monads'
|
5
|
+
require 'dry/configurable'
|
5
6
|
require 'concurrent'
|
6
7
|
require_relative '../leopard'
|
7
8
|
require_relative 'message_wrapper'
|
@@ -14,10 +15,14 @@ module Rubyists
|
|
14
15
|
|
15
16
|
def self.included(base)
|
16
17
|
base.extend(ClassMethods)
|
18
|
+
base.include(InstanceMethods)
|
17
19
|
base.extend(Dry::Monads[:result])
|
18
|
-
base.
|
20
|
+
base.extend(Dry::Configurable)
|
21
|
+
base.setting :logger, default: Rubyists::Leopard.logger, reader: true
|
19
22
|
end
|
20
23
|
|
24
|
+
Endpoint = Struct.new(:name, :subject, :queue, :group, :handler)
|
25
|
+
|
21
26
|
module ClassMethods
|
22
27
|
def endpoints = @endpoints ||= []
|
23
28
|
def groups = @groups ||= {}
|
@@ -33,13 +38,7 @@ module Rubyists
|
|
33
38
|
#
|
34
39
|
# @return [void]
|
35
40
|
def endpoint(name, subject: nil, queue: nil, group: nil, &handler)
|
36
|
-
endpoints <<
|
37
|
-
name:,
|
38
|
-
subject: subject || name,
|
39
|
-
queue:,
|
40
|
-
group:,
|
41
|
-
handler:,
|
42
|
-
}
|
41
|
+
endpoints << Endpoint.new(name:, subject: subject || name, queue:, group:, handler:)
|
43
42
|
end
|
44
43
|
|
45
44
|
# Define a group for organizing endpoints.
|
@@ -75,11 +74,12 @@ module Rubyists
|
|
75
74
|
# @return [void]
|
76
75
|
def run(nats_url:, service_opts:, instances: 1, blocking: true)
|
77
76
|
logger.info 'Booting NATS API server...'
|
78
|
-
|
79
|
-
pool = spawn_instances(nats_url, service_opts, instances)
|
77
|
+
workers = Concurrent::Array.new
|
78
|
+
pool = spawn_instances(nats_url, service_opts, instances, workers, blocking)
|
79
|
+
logger.info 'Setting up signal trap...'
|
80
|
+
trap_signals(workers, pool)
|
80
81
|
return pool unless blocking
|
81
82
|
|
82
|
-
# Otherwise, just sleep the main thread forever
|
83
83
|
sleep
|
84
84
|
end
|
85
85
|
|
@@ -90,21 +90,91 @@ module Rubyists
|
|
90
90
|
# @param url [String] The URL of the NATS server.
|
91
91
|
# @param opts [Hash] Options for the NATS service.
|
92
92
|
# @param count [Integer] The number of instances to spawn.
|
93
|
+
# @param workers [Array] The array to store worker instances.
|
94
|
+
# @param blocking [Boolean] If false, does not block current thread after starting the server.
|
93
95
|
#
|
94
96
|
# @return [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
95
|
-
def spawn_instances(url, opts, count)
|
97
|
+
def spawn_instances(url, opts, count, workers, blocking)
|
96
98
|
pool = Concurrent::FixedThreadPool.new(count)
|
99
|
+
@instance_args = opts.delete(:instance_args) || nil
|
100
|
+
logger.info "Building #{count} workers with options: #{opts.inspect}, instance_args: #{@instance_args}"
|
97
101
|
count.times do
|
98
|
-
|
99
|
-
gps = groups.dup
|
100
|
-
pool.post { setup_worker(url, opts, eps, gps) }
|
102
|
+
pool.post { build_worker(url, opts, workers, blocking) }
|
101
103
|
end
|
102
104
|
pool
|
103
105
|
end
|
104
106
|
|
107
|
+
# Builds a worker instance and sets it up with the NATS server.
|
108
|
+
#
|
109
|
+
# @param url [String] The URL of the NATS server.
|
110
|
+
# @param opts [Hash] Options for the NATS service.
|
111
|
+
# @param workers [Array] The array to store worker instances.
|
112
|
+
# @param blocking [Boolean] If true, blocks the current thread until the worker is set up.
|
113
|
+
#
|
114
|
+
# @return [void]
|
115
|
+
def build_worker(url, opts, workers, blocking)
|
116
|
+
worker = @instance_args ? new(*@instance_args) : new
|
117
|
+
workers << worker
|
118
|
+
return worker.setup_worker!(nats_url: url, service_opts: opts) if blocking
|
119
|
+
|
120
|
+
worker.setup_worker(nats_url: url, service_opts: opts)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Shuts down the NATS API server gracefully.
|
124
|
+
#
|
125
|
+
# @param workers [Array] The array of worker instances to stop.
|
126
|
+
# @param pool [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
127
|
+
#
|
128
|
+
# @return [Proc] A lambda that performs the shutdown operations.
|
129
|
+
def shutdown(workers, pool)
|
130
|
+
lambda do
|
131
|
+
logger.warn 'Draining worker subscriptions...'
|
132
|
+
workers.each(&:stop)
|
133
|
+
logger.warn 'All workers stopped, shutting down pool...'
|
134
|
+
pool.shutdown
|
135
|
+
logger.warn 'Pool is shut down, waiting for termination!'
|
136
|
+
pool.wait_for_termination
|
137
|
+
logger.warn 'Bye bye!'
|
138
|
+
wake_main_thread
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Sets up signal traps for graceful shutdown of the NATS API server.
|
143
|
+
#
|
144
|
+
# @param workers [Array] The array of worker instances to stop on signal.
|
145
|
+
# @param pool [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
146
|
+
#
|
147
|
+
# @return [void]
|
148
|
+
def trap_signals(workers, pool)
|
149
|
+
return if @trapped
|
150
|
+
|
151
|
+
%w[INT TERM QUIT].each do |sig|
|
152
|
+
trap(sig) do
|
153
|
+
logger.warn "Received #{sig} signal, shutting down..."
|
154
|
+
Thread.new { shutdown(workers, pool).call }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
@trapped = true
|
158
|
+
end
|
159
|
+
|
160
|
+
# Wakes up the main thread to allow it to continue execution after the server is stopped.
|
161
|
+
# This is useful when the server is running in a blocking mode.
|
162
|
+
# If the main thread is not blocked, this method does nothing.
|
163
|
+
#
|
164
|
+
# @return [void]
|
165
|
+
def wake_main_thread
|
166
|
+
Thread.main.wakeup
|
167
|
+
rescue ThreadError
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
module InstanceMethods
|
173
|
+
# Returns the logger configured for the NATS API server.
|
174
|
+
def logger = self.class.logger
|
175
|
+
|
105
176
|
# Sets up a worker thread for the NATS API server.
|
106
177
|
# This method connects to the NATS server, adds the service, groups, and endpoints,
|
107
|
-
# and keeps the worker thread alive.
|
108
178
|
#
|
109
179
|
# @param url [String] The URL of the NATS server.
|
110
180
|
# @param opts [Hash] Options for the NATS service.
|
@@ -112,63 +182,102 @@ module Rubyists
|
|
112
182
|
# @param gps [Hash] The groups to add.
|
113
183
|
#
|
114
184
|
# @return [void]
|
115
|
-
def setup_worker(
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
185
|
+
def setup_worker(nats_url: 'nats://localhost:4222', service_opts: {})
|
186
|
+
@thread = Thread.current
|
187
|
+
@client = NATS.connect nats_url
|
188
|
+
@service = @client.services.add(build_service_opts(service_opts:))
|
189
|
+
gps = self.class.groups.dup
|
190
|
+
eps = self.class.endpoints.dup
|
191
|
+
group_map = add_groups(gps)
|
192
|
+
add_endpoints eps, group_map
|
193
|
+
end
|
194
|
+
|
195
|
+
# Sets up a worker thread for the NATS API server and blocks the current thread.
|
196
|
+
#
|
197
|
+
# @see #setup_worker
|
198
|
+
def setup_worker!(nats_url: 'nats://localhost:4222', service_opts: {})
|
199
|
+
setup_worker(nats_url:, service_opts:)
|
121
200
|
sleep
|
122
201
|
end
|
123
202
|
|
203
|
+
# Stops the NATS API server worker.
|
204
|
+
def stop
|
205
|
+
@service&.stop
|
206
|
+
@client&.close
|
207
|
+
@thread&.wakeup
|
208
|
+
rescue ThreadError
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
# Builds the service options for the NATS service.
|
215
|
+
#
|
216
|
+
# @param service_opts [Hash] Options for the NATS service.
|
217
|
+
#
|
218
|
+
# @return [Hash] The complete service options including name and version.
|
219
|
+
def build_service_opts(service_opts:)
|
220
|
+
{
|
221
|
+
name: self.class.name.split('::').join('.'),
|
222
|
+
version: '0.1.0',
|
223
|
+
}.merge(service_opts)
|
224
|
+
end
|
225
|
+
|
124
226
|
# Adds groups to the NATS service.
|
125
227
|
#
|
126
|
-
# @param service [NATS::Service] The NATS service to add groups to.
|
127
228
|
# @param gps [Hash] The groups to add, where keys are group names and values are group definitions.
|
128
229
|
#
|
129
230
|
# @return [Hash] A map of group names to their created group objects.
|
130
|
-
def add_groups(
|
231
|
+
def add_groups(gps)
|
131
232
|
created = {}
|
132
|
-
gps.each_key { |name| build_group(
|
233
|
+
gps.each_key { |name| build_group(gps, created, name) }
|
133
234
|
created
|
134
235
|
end
|
135
236
|
|
136
237
|
# Builds a group in the NATS service.
|
137
238
|
#
|
138
|
-
# @param service [NATS::Service] The NATS service to add the group to.
|
139
239
|
# @param defs [Hash] The group definitions, where keys are group names and values are group definitions.
|
140
240
|
# @param cache [Hash] A cache to store already created groups.
|
141
241
|
# @param name [String] The name of the group to build.
|
142
242
|
#
|
143
243
|
# @return [NATS::Group] The created group object.
|
144
|
-
def build_group(
|
244
|
+
def build_group(defs, cache, name)
|
145
245
|
return cache[name] if cache.key?(name)
|
146
246
|
|
147
247
|
gdef = defs[name]
|
148
248
|
raise ArgumentError, "Group #{name} not defined" unless gdef
|
149
249
|
|
150
|
-
parent = gdef[:parent] ? build_group(
|
250
|
+
parent = gdef[:parent] ? build_group(defs, cache, gdef[:parent]) : @service
|
151
251
|
cache[name] = parent.groups.add(gdef[:name], queue: gdef[:queue])
|
152
252
|
end
|
153
253
|
|
154
254
|
# Adds endpoints to the NATS service.
|
155
255
|
#
|
156
|
-
# @param service [NATS::Service] The NATS service to add endpoints to.
|
157
256
|
# @param endpoints [Array<Hash>] The list of endpoints to add.
|
158
257
|
# @param group_map [Hash] A map of group names to their created group objects.
|
159
258
|
#
|
160
259
|
# @return [void]
|
161
|
-
def add_endpoints(
|
260
|
+
def add_endpoints(endpoints, group_map)
|
162
261
|
endpoints.each do |ep|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
262
|
+
grp = ep.group
|
263
|
+
parent = grp ? group_map[grp] : @service
|
264
|
+
raise ArgumentError, "Group #{grp} not defined" if grp && parent.nil?
|
265
|
+
|
266
|
+
build_endpoint(parent, ep)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Builds an endpoint in the NATS service.
|
271
|
+
#
|
272
|
+
# @param parent [NATS::Group] The parent group or service to add the endpoint to.
|
273
|
+
# @param ept [Endpoint] The endpoint definition containing name, subject, queue, and handler.
|
274
|
+
# NOTE: Named ept because `endpoint` is a DSL method we expose, to avoid confusion.
|
275
|
+
#
|
276
|
+
# @return [void]
|
277
|
+
def build_endpoint(parent, ept)
|
278
|
+
parent.endpoints.add(ept.name, subject: ept.subject, queue: ept.queue) do |raw_msg|
|
279
|
+
wrapper = MessageWrapper.new(raw_msg)
|
280
|
+
dispatch_with_middleware(wrapper, ept.handler)
|
172
281
|
end
|
173
282
|
end
|
174
283
|
|
@@ -180,7 +289,7 @@ module Rubyists
|
|
180
289
|
# @return [void]
|
181
290
|
def dispatch_with_middleware(wrapper, handler)
|
182
291
|
app = ->(w) { handle_message(w.raw, handler) }
|
183
|
-
middleware.reverse_each do |(klass, args, blk)|
|
292
|
+
self.class.middleware.reverse_each do |(klass, args, blk)|
|
184
293
|
app = klass.new(app, *args, &blk)
|
185
294
|
end
|
186
295
|
app.call(wrapper)
|
@@ -203,7 +312,6 @@ module Rubyists
|
|
203
312
|
|
204
313
|
# Processes the result of the handler execution.
|
205
314
|
#
|
206
|
-
#
|
207
315
|
# @param wrapper [MessageWrapper] The message wrapper containing the raw message.
|
208
316
|
# @param result [Dry::Monads::Result] The result of the handler execution.
|
209
317
|
#
|
data/lib/leopard/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leopard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- bougyman
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: concurrent-ruby
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- ci/nats/accounts.txt
|
99
99
|
- ci/nats/start.sh
|
100
100
|
- ci/publish-gem.sh
|
101
|
+
- doc/service-api-vs-rest.adoc
|
101
102
|
- examples/echo_endpoint.rb
|
102
103
|
- lib/leopard.rb
|
103
104
|
- lib/leopard/errors.rb
|