Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c4b1985
feat: adds main lib and errors classes
bougyman Jul 27, 2025
c4e4ec1
chore: Adds group to Gemfile
bougyman Jul 29, 2025
7a4fccc
feat: Adds basic message wrapper
bougyman Jul 29, 2025
015af76
feat: Basic api server using message wrapper
bougyman Jul 29, 2025
21f66db
test: Adds test for message wrapper
bougyman Jul 29, 2025
f6b877d
test: Adds tests for nats_api_server
bougyman Jul 29, 2025
3db03e7
doc: Fills in Readme with current functionality
bougyman Jul 29, 2025
ac6cfe1
fix: Remove redundant include of Dry::Configurable
bougyman Jul 29, 2025
fa18aa2
doc: Adds link for Dry::Configurable and removes useless internal set…
bougyman Jul 29, 2025
e44ecc6
feat: Ditch Ractors for Concurrent::FixedThreadPool
bougyman Jul 30, 2025
80683c2
feat: Adds #group to namespace endpoints
bougyman Jul 30, 2025
d32da82
chore: Be more leniant with documentation commit messages
bougyman Jul 30, 2025
bf13d94
docs: Updates Readme about the change to Concurrent::FixedThreadPool
bougyman Jul 30, 2025
9136639
docs: Adds bits about Dry::Monads
bougyman Jul 30, 2025
fd456c2
docs: Clarifies endpoint mappings
bougyman Jul 30, 2025
dcbf2b4
fix: Fixes example echo service
bougyman Jul 30, 2025
114d70e
fix: Got monads at all scope levels
bougyman Jul 30, 2025
549edc6
fix: Extend the base module to reduce boilerplate
bougyman Jul 30, 2025
3781a6e
fix: Get semantic logger out of the example
bougyman Jul 30, 2025
565ab55
feat: Adds some basic logging
bougyman Jul 30, 2025
1e8b0e4
fix: Do not auto require nats api server itself
bougyman Jul 31, 2025
7efe571
docs: Added method documentation
bougyman Jul 31, 2025
777612b
Merge branch 'main' into filling-in-functionality
bougyman Jul 31, 2025
f09b4f8
docs: Important note about the callback return expectation
bougyman Jul 31, 2025
746c72a
style: Rubocop fixes
bougyman Jul 31, 2025
3755f51
test: Removes the #run from specs for now, stubbing wasn not working
bougyman Jul 31, 2025
9642c8c
feat: Adds non-blocking mode
bougyman Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.gem
*.rbc
*.swp
/.config
/coverage/
/InstalledFiles
Expand Down
22 changes: 12 additions & 10 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in sequel-pgt_outbox.gemspec
gemspec

gem 'minitest'
gem 'minitest-global_expectations'
gem 'pry'
gem 'rake'
gem 'reline'
gem 'rubocop'
gem 'rubocop-minitest'
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'simplecov'
group :development, :test do
gem 'minitest'
gem 'minitest-global_expectations'
gem 'pry'
gem 'rake'
gem 'reline'
gem 'rubocop'
gem 'rubocop-minitest'
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'simplecov'
end
19 changes: 19 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ PATH
remote: .
specs:
leopard (0.1.0)
concurrent-ruby (~> 1.1)
dry-configurable (~> 1.3)
dry-monads (~> 1.9)
nats-pure (~> 2.5)
semantic_logger (~> 4)

GEM
remote: https://rubygems.org/
Expand All @@ -12,10 +16,22 @@ GEM
coderay (1.1.3)
concurrent-ruby (1.3.5)
docile (1.4.1)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-monads (1.9.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
io-console (0.8.1)
json (2.13.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
method_source (1.1.0)
minitest (5.25.5)
minitest-global_expectations (1.0.1)
Expand Down Expand Up @@ -68,6 +84,8 @@ GEM
rubocop (>= 1.72.1)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
semantic_logger (4.17.0)
concurrent-ruby (~> 1.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
Expand All @@ -79,6 +97,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
zeitwerk (2.7.3)

PLATFORMS
ruby
Expand Down
114 changes: 107 additions & 7 deletions Readme.adoc
Original file line number Diff line number Diff line change
@@ -1,9 +1,109 @@
# Leopard Nats ServiceApi Server
= Leopard NATS ServiceApi Server
bougyman <[email protected]>
:service-api: https://github.com/rubyists/nats-pure.rb/blob/main/docs/service_api.md[Service API]
:service-api: https://github.com/rubyists/nats-pure.rb/blob/main/docs/service_api.md[NATS Service API]
:conventional-commits: https://www.conventionalcommits.org/en/v1.0.0/[Conventional Commits]
:dry-configurable: https://github.com/dry-rb/dry-configurable[Dry::Configurable]
:dry-monads: https://github.com/dry-rb/dry-monads[Dry::Monads]

The leopard nats serviceapi server provides a simple concurrency
model for NATS {service-api} workers. It is designed to be used
similarly to a web server (inspired by puma), defining endpoints
in your classes, and then serving them via the leopard (Ractor-based)
service supervisor.
Leopard is a small framework for building concurrent {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`.
* Middleware support using `use`.
* Simple concurrency via `run` with a configurable number of instances.
* JSON aware message wrapper that gracefully handles parse errors.
* Railway Oriented Design, using {dry-monads} for success and failure handling.
* {dry-configurable} settings container.

== Requirements

* Ruby >= 3.3.0
* A running NATS server with the Service API enabled.

== Installation

Add the gem to your project:

[source,ruby]
----
# Gemfile
gem 'leopard'
----

Then install it with Bundler.

[source,bash]
----
$ 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 {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`.

[source,ruby]
----
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:

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

Middleware can be inserted around endpoint dispatch:

[source,ruby]
----
class LoggerMiddleware
def initialize(app)
@app = app
end

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

EchoService.use LoggerMiddleware
----

== Development

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

[source,bash]
----
$ bundle exec rake
----

=== 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
27 changes: 27 additions & 0 deletions ci/nats/accounts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Client port of 4222 on all interfaces
port: 4222

# HTTP monitoring port
monitor_port: 8222

accounts: {
$SYS: {
users: [
{ user: sys, password: sys }
]
}
ME: {
jetstream: enabled
users: [
{ user: me, password: youandme }
]
}
}
no_auth_user: me

authorization {
default_permissions = {
publish = ">"
subscribe = ">"
}
}
33 changes: 33 additions & 0 deletions ci/nats/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash

NATS_VERSION=2

if readlink -f . >/dev/null 2>&1 # {{{ makes readlink work on mac
then
readlink=readlink
else
if greadlink -f . >/dev/null 2>&1
then
readlink=greadlink
else
printf "You must install greadlink to use this (brew install coreutils)\n" >&2
fi
fi # }}}

# Set here to the full path to this script
me=${BASH_SOURCE[0]}
[ -L "$me" ] && me=$($readlink -f "$me")
here=$(cd "$(dirname "$me")" && pwd)
just_me=$(basename "$me")
export just_me

cd "$here" || exit 1
if command -v podman 2>/dev/null
then
runtime=podman
else
runtime=docker
fi

set -x
exec "$runtime" run --rm -it -p 4222:4222 -p 6222:6222 -p 8222:8222 -v ./accounts.txt:/accounts.txt nats:"$NATS_VERSION" -js -c /accounts.txt "$@"
2 changes: 1 addition & 1 deletion ci/publish-gem.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ me=${BASH_SOURCE[0]}
here=$(cd "$(dirname "$me")" && pwd)
just_me=$(basename "$me")

: "${GEM_NAME:=sequel-pgt_outbox}"
: "${GEM_NAME:=leopard}"
: "${GIT_ORG:=rubyists}"

GEM_HOST=$1
Expand Down
19 changes: 19 additions & 0 deletions examples/echo_endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative '../lib/leopard/nats_api_server'

# Example to echo the given message
class EchoService
include Rubyists::Leopard::NatsApiServer

endpoint(:echo) { |msg| Success(msg.data) }
end

if __FILE__ == $PROGRAM_NAME
EchoService.run(
nats_url: 'nats://localhost:4222',
service_opts: { name: 'example.echo', version: '1.0.0' },
instances: 4,
)
end
6 changes: 5 additions & 1 deletion leopard.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require_relative 'lib/leopard/version'

Gem::Specification.new do |spec|
Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
spec.name = 'leopard'
spec.version = Rubyists::Leopard::VERSION
spec.authors = ['bougyman']
Expand Down Expand Up @@ -34,7 +34,11 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']

# Uncomment to register a new dependency of your gem
spec.add_dependency 'concurrent-ruby', '~> 1.1'
spec.add_dependency 'dry-configurable', '~> 1.3'
spec.add_dependency 'dry-monads', '~> 1.9'
spec.add_dependency 'nats-pure', '~> 2.5'
spec.add_dependency 'semantic_logger', '~> 4'

# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
Expand Down
21 changes: 21 additions & 0 deletions lib/leopard.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'dry/configurable'
require 'pathname'
require 'semantic_logger'
SemanticLogger.add_appender(io: $stdout, formatter: :color)

class Pathname
def /(other)
join other.to_s
end
end

module Rubyists
module Leopard
end
end

require_relative 'leopard/settings'
require_relative 'leopard/version'
require_relative 'leopard/errors'
8 changes: 8 additions & 0 deletions lib/leopard/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module Rubyists
module Leopard
class Error < StandardError; end
class ConfigurationError < Error; end
end
end
Loading