Leopard is a small framework for building concurrent NATS Service API workers. It uses Concurrent::FixedThreadPool to manage multiple workers in a single process and provides a minimal DSL for defining endpoints and middleware.

Features

  • Declarative endpoint definitions with #endpoint.

  • Declarative JetStream pull consumers with #jetstream_endpoint.

  • Grouping of endpoints with #group

  • Simple concurrency via #run with a configurable number of instances.

  • JSON aware message wrapper that gracefully handles parse errors.

  • Middleware support using #use.

  • Railway Oriented Design, using Dry::Monads for success and failure handling.

  • Dry::Configurable settings container.

  • #logger defaults to SemanticLogger (adjustable as the #logger= setting)

Requirements

  • Ruby >= 3.4.0

  • A running NATS server with the Service API enabled.

Installation

Add the gem to your project:

# Gemfile
gem 'leopard'

Then install it with Bundler.

$ bundle install

Usage

Create a service class and include Rubyists::Leopard::NatsApiServer. Define one or more endpoints. Each endpoint receives a Rubyists::Leopard::MessageWrapper object for each request to the NATS Service API endpoint that service class is is subscribed to (subject:, or name:). The message handler/callback is expected to return a Dry::Monads[:result] object, typically a Success or Failure.

class EchoService
  include Rubyists::Leopard::NatsApiServer

  endpoint :echo do |msg|
    Success(msg.data)
  end
end

Run the service by providing the NATS connection details and service options:

EchoService.run(
  nats_url: 'nats://localhost:4222',
  service_opts: { name: 'echo' },
  instances: 4
)

Middleware can be inserted around endpoint dispatch:

class LoggerMiddleware
  def initialize(app)
    @app = app
  end

  def call(wrapper)
    puts "received: #{wrapper.data.inspect}"
    @app.call(wrapper)
  end
end

EchoService.use LoggerMiddleware

JetStream Pull Consumers

Leopard can also bind JetStream pull consumers through the same middleware and Dry::Monads::Result handler contract used by request/reply endpoints.

class EventConsumer
  include Rubyists::Leopard::NatsApiServer

  jetstream_endpoint(
    :events,
    stream: 'EVENTS',
    subject: 'events.created',
    durable: 'events-created-worker',
    consumer: { max_deliver: 5 },
    batch: 5,
    fetch_timeout: 1,
    nak_delay: 2,
  ) do |msg|
    Success(msg.data)
  end
end

JetStream handlers receive the same Rubyists::Leopard::MessageWrapper as service endpoints. Leopard will:

  • ack on Success

  • nak on Failure (nak_delay: is optional)

  • term on unhandled exceptions

Each Leopard instances: worker creates its own pull subscription loop, so JetStream consumers scale with the same process-local concurrency model as the rest of the framework.

Development

The project uses Minitest and RuboCop. Run tests with Rake:

$ bundle exec rake ci

This task starts NATS JetStream through ./ci/nats/start.sh, waits for broker health, runs RuboCop and the test suite, and then stops the broker.

API documentation can be generated with:

$ bundle exec rake yard

Documentation coverage is enforced with:

$ bundle exec rake yard:verify

If you want to run the broker yourself, the same script can still be used directly:

$ ./ci/nats/start.sh

Conventional Commits (semantic commit messages)

This project follows the Conventional Commits specification.

To contribute, please follow that commit message format, or your pull request may be rejected.

License

MIT