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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cb911c6ac11f486f56610fd734ae2a4e5187495d28524621d742fca976377eb
4
- data.tar.gz: 713a3ea176a74f8a47abc83c4340a9432b781262b9fd5e431ada44991d9c2395
3
+ metadata.gz: 343e274b8fffbe093d59b1d819a4b0be0ba9efe109c4883c535ba3692f1854a3
4
+ data.tar.gz: 6b32780cf1bd40d7b0191a79a533382dbc6ed828162d9c6ebeefa2c838191c77
5
5
  SHA512:
6
- metadata.gz: ee37dc08e56cb6dc95262c0c61df02ad8f9fc2168cfff2c30fcf908dfb51c502676e796d00e2903bc9c8251d1d866b829579abce5fca3172336f1a74fb18b1a6
7
- data.tar.gz: f09054e25ce41281175c0d09ccc993ec735f2c3278c9d631d964aa39850fc9fdfd54b08144c67df4d938079245f60a5da0079fda68fb209131f83384c8e421f8
6
+ metadata.gz: 2ee58b9e7a03d9dee24f3d338e700c0d0456ff0056379e06405418365023a14dc7484bad768d7deff2727cb7e01f0daa331234bfa6b0875b5891f79577a1880e
7
+ data.tar.gz: '047850ac66d9a789bdaad311ef764bed2bea365776a67d6f47f9634eb039edc303ab48d6227ad443f4f2a49b5fba1f1cd63c5d45068120b544d9a1e8a65c292d'
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.1.5"
2
+ ".": "0.1.7"
3
3
  }
data/.version.txt CHANGED
@@ -1 +1 @@
1
- 0.1.5
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.
@@ -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: { name: 'example.echo', version: '1.0.0' },
17
- instances: 4,
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, code: 500)
37
- raw.respond_with_error(err.to_s, code:)
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.include(SemanticLogger::Loggable)
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
- # Return the thread pool if non-blocking
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
- eps = endpoints.dup
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(url, opts, eps, gps)
116
- client = NATS.connect url
117
- service = client.services.add(**opts)
118
- group_map = add_groups(service, gps)
119
- add_endpoints service, eps, group_map
120
- # Keep the worker thread alive
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(service, gps)
231
+ def add_groups(gps)
131
232
  created = {}
132
- gps.each_key { |name| build_group(service, gps, created, name) }
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(service, defs, cache, name)
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(service, defs, cache, gdef[:parent]) : service
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(service, endpoints, group_map)
260
+ def add_endpoints(endpoints, group_map)
162
261
  endpoints.each do |ep|
163
- parent = ep[:group] ? group_map[ep[:group]] : service
164
- raise ArgumentError, "Group #{ep[:group]} not defined" if ep[:group] && parent.nil?
165
-
166
- parent.endpoints.add(
167
- ep[:name], subject: ep[:subject], queue: ep[:queue]
168
- ) do |raw_msg|
169
- wrapper = MessageWrapper.new(raw_msg)
170
- dispatch_with_middleware(wrapper, ep[:handler])
171
- end
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
  #
@@ -3,7 +3,7 @@
3
3
  module Rubyists
4
4
  module Leopard
5
5
  # x-release-please-start-version
6
- VERSION = '0.1.5'
6
+ VERSION = '0.1.7'
7
7
  # x-release-please-end
8
8
  end
9
9
  end
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.5
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-07-31 00:00:00.000000000 Z
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