Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/container.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
max-parallel: 5
matrix:
alpine-version: ['3.20']
ruby-version: ['3.4.1']
ruby-version: ['4.0.2']
steps:
-
name: Checkout repository
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gem.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
ruby-version: 4.0.2
bundler-cache: false
-
name: Publish to ${{ matrix.registry }}
Expand Down
13 changes: 6 additions & 7 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ jobs:
matrix:
ruby:
- '3.4.5'
services:
nats:
image: nats:latest
ports: ["4222:4222", "6222:6222", "8222:8222"]
- '4.0.2'
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
Expand All @@ -26,7 +23,9 @@ jobs:
bundler-cache: false
- name: Install dependencies
run: bundle install --jobs 4 --retry 3
- name: Run the default task
run: bundle exec rake
- name: Run CI task
run: bundle exec rake ci
env:
NATS_URI: nats://nats:4222
NATS_URI: nats://127.0.0.1:4222
LEOPARD_NATS_URL: nats://127.0.0.1:4222
NATS_NAME: leopard-nats
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ Gemfile.lock
.rvmrc

# Used by RuboCop. Remote config files pulled in from inherit_from directive.
.rubocop_cache
# .rubocop-https?--*
5 changes: 5 additions & 0 deletions .yardopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
--readme Readme.adoc
--protected
--private
--output-dir doc/yard
lib/**/*.rb
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ source 'https://rubygems.org'
gemspec

group :development, :test do
gem 'asciidoctor'
gem 'irb'
gem 'minitest'
gem 'minitest-global_expectations'
gem 'minitest-mock'
gem 'pry'
gem 'rake'
gem 'reline'
Expand All @@ -16,4 +19,5 @@ group :development, :test do
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'simplecov'
gem 'yard'
end
37 changes: 33 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
leopard (0.2.4)
leopard (0.2.5)
concurrent-ruby (~> 1.1)
dry-configurable (~> 1.3)
dry-monads (~> 1.9)
Expand All @@ -11,11 +11,14 @@ PATH
GEM
remote: https://rubygems.org/
specs:
asciidoctor (2.0.26)
ast (2.4.3)
base64 (0.3.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
date (3.5.1)
docile (1.4.1)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
Expand All @@ -27,15 +30,24 @@ GEM
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
erb (6.0.3)
io-console (0.8.1)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.13.2)
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 (6.0.4)
drb (~> 2.0)
prism (~> 1.5)
minitest-global_expectations (1.0.1)
minitest (> 5)
minitest-mock (5.27.0)
nats-pure (2.5.0)
base64
concurrent-ruby (~> 1.0)
Expand All @@ -46,13 +58,23 @@ GEM
parser (3.3.9.0)
ast (~> 2.4.1)
racc
prism (1.4.0)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.3.1)
date
stringio
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.0)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.2)
reline (0.6.2)
io-console (~> 0.5)
Expand Down Expand Up @@ -91,20 +113,26 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
stringio (3.2.0)
tsort (0.2.0)
unicode-display_width (3.1.5)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicode-emoji (4.2.0)
uri (1.0.3)
yard (0.9.41)
zeitwerk (2.7.3)

PLATFORMS
ruby
x86_64-linux

DEPENDENCIES
asciidoctor
irb
leopard!
minitest
minitest-global_expectations
minitest-mock
pry
rake
reline
Expand All @@ -113,6 +141,7 @@ DEPENDENCIES
rubocop-performance
rubocop-rake
simplecov
yard

BUNDLED WITH
2.6.9
111 changes: 110 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ require 'rake'
require 'minitest/test_task'
require 'bundler/gem_tasks'
require 'rubocop/rake_task'
require 'net/http'
require 'open3'
require 'shellwords'
require 'timeout'
require 'yard'
require 'yard/rake/yardoc_task'

RuboCop::RakeTask.new
YARD::Rake::YardocTask.new(:yard)

Minitest::TestTask.create(:test) do |task|
task.libs << 'lib'
Expand All @@ -14,4 +21,106 @@ Minitest::TestTask.create(:test) do |task|
task.warning = true
end

task default: %i[rubocop test]
QUICK_TEST_FILES = Dir['test/*/**/*.rb'].reject { |file| file.start_with?('test/integration/') }.sort.freeze

# Returns the local NATS JetStream health endpoint used by the CI helpers.
#
# @return [URI::HTTP] The health endpoint URI.
def nats_health_uri = URI('http://127.0.0.1:8222/healthz')

# Reports whether the local NATS JetStream health endpoint is currently reachable.
#
# @return [Boolean] `true` when the broker responds successfully, otherwise `false`.
def nats_ready?
Net::HTTP.get_response(nats_health_uri).is_a?(Net::HTTPSuccess)
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET
false
end

# Waits for the local NATS JetStream broker to report healthy.
#
# @return [void]
# @raise [RuntimeError] If the broker does not become healthy within 30 seconds.
def wait_for_nats!
Timeout.timeout(30) do
sleep 1 until nats_ready?
end
rescue Timeout::Error
raise 'Timed out waiting for NATS JetStream health endpoint on http://127.0.0.1:8222/healthz'
end

# Detects the container runtime used to manage the local NATS broker.
#
# @return [String] `podman` when available, otherwise `docker`.
def container_runtime
File.executable?('/usr/bin/podman') || system('command -v podman > /dev/null 2>&1', exception: false) ? 'podman' : 'docker'
end

# Runs the non-integration test files directly for a fast local feedback loop.
#
# @return [void]
def run_quick_tests!
sh "ruby -w -Ilib -Itest #{QUICK_TEST_FILES.shelljoin}"
end

# Verifies that the current YARD coverage is complete.
#
# @return [void]
# @raise [RuntimeError] If YARD reports anything less than 100% documentation coverage.
def verify_yard_coverage!
output, status = Open3.capture2e('bundle', 'exec', 'yard', 'stats', '--list-undoc')
puts output
raise 'yard stats failed' unless status.success?
return if output.include?('100.00% documented')

raise 'YARD documentation coverage is incomplete'
end

namespace :nats do
desc 'Start the local NATS JetStream broker via ./ci/nats/start.sh'
task :start do
sh({ 'NATS_DETACH' => '1' }, './ci/nats/start.sh')
end

desc 'Wait for the local NATS JetStream broker health endpoint'
task :wait do
wait_for_nats!
end

desc 'Stop the local NATS JetStream broker container'
task :stop do
name = ENV.fetch('NATS_NAME', 'leopard-nats')
sh(container_runtime, 'rm', '-f', name, verbose: false)
rescue RuntimeError
nil
end
end

namespace :ci do
desc 'Run RuboCop, YARD verification, and the non-integration test suite without managing NATS'
task quick: %i[rubocop yard:verify] do
run_quick_tests!
end

desc 'Run the full test suite against a managed local NATS JetStream broker'
task :test do
Rake::Task['nats:start'].invoke
Rake::Task['nats:wait'].invoke
Rake::Task['test'].invoke
ensure
Rake::Task['nats:stop'].reenable
Rake::Task['nats:stop'].invoke
end
end

desc 'Run RuboCop and the full test suite against a managed local NATS JetStream broker'
task ci: %w[rubocop yard:verify ci:test]

namespace :yard do
desc 'Fail if YARD reports incomplete documentation coverage'
task :verify do
verify_yard_coverage!
end
end

task default: :ci
62 changes: 61 additions & 1 deletion Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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.
Expand Down Expand Up @@ -90,13 +91,72 @@ 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.

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

[source,bash]
----
$ bundle exec 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:

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

Documentation coverage is enforced with:

[source,bash]
----
$ bundle exec rake yard:verify
----

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

[source,bash]
----
$ ./ci/nats/start.sh
----

=== Conventional Commits (semantic commit messages)
Expand Down
Loading
Loading