Skip to content

Commit 9ff0942

Browse files
authored
feat: Basic functionality for serving apis (#4)
* feat: adds main lib and errors classes * chore: Adds group to Gemfile feat: Adds settings to module fix: Corrects gemname in publish-gem.sh * feat: Adds basic message wrapper * feat: Basic api server using message wrapper * test: Adds test for message wrapper * test: Adds tests for nats_api_server * doc: Fills in Readme with current functionality * fix: Remove redundant include of Dry::Configurable * doc: Adds link for Dry::Configurable and removes useless internal settings nonsense * feat: Ditch Ractors for Concurrent::FixedThreadPool * feat: Adds #group to namespace endpoints * chore: Be more leniant with documentation commit messages * docs: Updates Readme about the change to Concurrent::FixedThreadPool * docs: Adds bits about Dry::Monads * docs: Clarifies endpoint mappings * fix: Fixes example echo service * fix: Got monads at all scope levels * fix: Extend the base module to reduce boilerplate * fix: Get semantic logger out of the example * feat: Adds some basic logging * fix: Do not auto require nats api server itself * docs: Added method documentation * docs: Important note about the callback return expectation * style: Rubocop fixes * test: Removes the #run from specs for now, stubbing wasn not working * feat: Adds non-blocking mode
1 parent 5f4732d commit 9ff0942

20 files changed

+778
-30
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*.gem
22
*.rbc
3+
*.swp
34
/.config
45
/coverage/
56
/InstalledFiles

Gemfile

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ source 'https://rubygems.org'
55
# Specify your gem's dependencies in sequel-pgt_outbox.gemspec
66
gemspec
77

8-
gem 'minitest'
9-
gem 'minitest-global_expectations'
10-
gem 'pry'
11-
gem 'rake'
12-
gem 'reline'
13-
gem 'rubocop'
14-
gem 'rubocop-minitest'
15-
gem 'rubocop-performance'
16-
gem 'rubocop-rake'
17-
gem 'simplecov'
8+
group :development, :test do
9+
gem 'minitest'
10+
gem 'minitest-global_expectations'
11+
gem 'pry'
12+
gem 'rake'
13+
gem 'reline'
14+
gem 'rubocop'
15+
gem 'rubocop-minitest'
16+
gem 'rubocop-performance'
17+
gem 'rubocop-rake'
18+
gem 'simplecov'
19+
end

Gemfile.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ PATH
22
remote: .
33
specs:
44
leopard (0.1.0)
5+
concurrent-ruby (~> 1.1)
6+
dry-configurable (~> 1.3)
7+
dry-monads (~> 1.9)
58
nats-pure (~> 2.5)
9+
semantic_logger (~> 4)
610

711
GEM
812
remote: https://rubygems.org/
@@ -12,10 +16,22 @@ GEM
1216
coderay (1.1.3)
1317
concurrent-ruby (1.3.5)
1418
docile (1.4.1)
19+
dry-configurable (1.3.0)
20+
dry-core (~> 1.1)
21+
zeitwerk (~> 2.6)
22+
dry-core (1.1.0)
23+
concurrent-ruby (~> 1.0)
24+
logger
25+
zeitwerk (~> 2.6)
26+
dry-monads (1.9.0)
27+
concurrent-ruby (~> 1.0)
28+
dry-core (~> 1.1)
29+
zeitwerk (~> 2.6)
1530
io-console (0.8.1)
1631
json (2.13.1)
1732
language_server-protocol (3.17.0.5)
1833
lint_roller (1.1.0)
34+
logger (1.7.0)
1935
method_source (1.1.0)
2036
minitest (5.25.5)
2137
minitest-global_expectations (1.0.1)
@@ -68,6 +84,8 @@ GEM
6884
rubocop (>= 1.72.1)
6985
ruby-progressbar (1.13.0)
7086
securerandom (0.4.1)
87+
semantic_logger (4.17.0)
88+
concurrent-ruby (~> 1.0)
7189
simplecov (0.22.0)
7290
docile (~> 1.1)
7391
simplecov-html (~> 0.11)
@@ -79,6 +97,7 @@ GEM
7997
unicode-emoji (~> 4.0, >= 4.0.4)
8098
unicode-emoji (4.0.4)
8199
uri (1.0.3)
100+
zeitwerk (2.7.3)
82101

83102
PLATFORMS
84103
ruby

Readme.adoc

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,109 @@
1-
# Leopard Nats ServiceApi Server
1+
= Leopard NATS ServiceApi Server
22
bougyman <me@bougyman.com>
3-
:service-api: https://github.com/rubyists/nats-pure.rb/blob/main/docs/service_api.md[Service API]
3+
:service-api: https://github.com/rubyists/nats-pure.rb/blob/main/docs/service_api.md[NATS Service API]
4+
:conventional-commits: https://www.conventionalcommits.org/en/v1.0.0/[Conventional Commits]
5+
:dry-configurable: https://github.com/dry-rb/dry-configurable[Dry::Configurable]
6+
:dry-monads: https://github.com/dry-rb/dry-monads[Dry::Monads]
47

5-
The leopard nats serviceapi server provides a simple concurrency
6-
model for NATS {service-api} workers. It is designed to be used
7-
similarly to a web server (inspired by puma), defining endpoints
8-
in your classes, and then serving them via the leopard (Ractor-based)
9-
service supervisor.
8+
Leopard is a small framework for building concurrent {service-api} workers.
9+
It uses `Concurrent::FixedThreadPool` to manage multiple workers in a single process and provides a
10+
minimal DSL for defining endpoints and middleware.
11+
12+
== Features
13+
14+
* Declarative endpoint definitions with `endpoint`.
15+
* Middleware support using `use`.
16+
* Simple concurrency via `run` with a configurable number of instances.
17+
* JSON aware message wrapper that gracefully handles parse errors.
18+
* Railway Oriented Design, using {dry-monads} for success and failure handling.
19+
* {dry-configurable} settings container.
20+
21+
== Requirements
22+
23+
* Ruby >= 3.3.0
24+
* A running NATS server with the Service API enabled.
25+
26+
== Installation
27+
28+
Add the gem to your project:
29+
30+
[source,ruby]
31+
----
32+
# Gemfile
33+
gem 'leopard'
34+
----
35+
36+
Then install it with Bundler.
37+
38+
[source,bash]
39+
----
40+
$ bundle install
41+
----
42+
43+
== Usage
44+
45+
Create a service class and include `Rubyists::Leopard::NatsApiServer`.
46+
Define one or more endpoints. Each endpoint receives a
47+
`Rubyists::Leopard::MessageWrapper` object for each request to the {service-api} endpoint
48+
that service class is is subscribed to (subject:, or name:). The message handler/callback
49+
is expected to return a `Dry::Monads[:result]` object, typically a `Success` or `Failure`.
50+
51+
[source,ruby]
52+
----
53+
class EchoService
54+
include Rubyists::Leopard::NatsApiServer
55+
56+
endpoint :echo do |msg|
57+
Success(msg.data)
58+
end
59+
end
60+
----
61+
62+
Run the service by providing the NATS connection details and service options:
63+
64+
[source,ruby]
65+
----
66+
EchoService.run(
67+
nats_url: 'nats://localhost:4222',
68+
service_opts: { name: 'echo' },
69+
instances: 4
70+
)
71+
----
72+
73+
Middleware can be inserted around endpoint dispatch:
74+
75+
[source,ruby]
76+
----
77+
class LoggerMiddleware
78+
def initialize(app)
79+
@app = app
80+
end
81+
82+
def call(wrapper)
83+
puts "received: #{wrapper.data.inspect}"
84+
@app.call(wrapper)
85+
end
86+
end
87+
88+
EchoService.use LoggerMiddleware
89+
----
90+
91+
== Development
92+
93+
The project uses Minitest and RuboCop. Run tests with Rake:
94+
95+
[source,bash]
96+
----
97+
$ bundle exec rake
98+
----
99+
100+
=== Conventional Commits (semantic commit messages)
101+
102+
This project follows the {conventional-commits} specification.
103+
104+
To contribute, please follow that commit message format,
105+
or your pull request may be rejected.
106+
107+
== License
108+
109+
MIT

ci/nats/accounts.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Client port of 4222 on all interfaces
2+
port: 4222
3+
4+
# HTTP monitoring port
5+
monitor_port: 8222
6+
7+
accounts: {
8+
$SYS: {
9+
users: [
10+
{ user: sys, password: sys }
11+
]
12+
}
13+
ME: {
14+
jetstream: enabled
15+
users: [
16+
{ user: me, password: youandme }
17+
]
18+
}
19+
}
20+
no_auth_user: me
21+
22+
authorization {
23+
default_permissions = {
24+
publish = ">"
25+
subscribe = ">"
26+
}
27+
}

ci/nats/start.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
3+
NATS_VERSION=2
4+
5+
if readlink -f . >/dev/null 2>&1 # {{{ makes readlink work on mac
6+
then
7+
readlink=readlink
8+
else
9+
if greadlink -f . >/dev/null 2>&1
10+
then
11+
readlink=greadlink
12+
else
13+
printf "You must install greadlink to use this (brew install coreutils)\n" >&2
14+
fi
15+
fi # }}}
16+
17+
# Set here to the full path to this script
18+
me=${BASH_SOURCE[0]}
19+
[ -L "$me" ] && me=$($readlink -f "$me")
20+
here=$(cd "$(dirname "$me")" && pwd)
21+
just_me=$(basename "$me")
22+
export just_me
23+
24+
cd "$here" || exit 1
25+
if command -v podman 2>/dev/null
26+
then
27+
runtime=podman
28+
else
29+
runtime=docker
30+
fi
31+
32+
set -x
33+
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 "$@"

ci/publish-gem.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ me=${BASH_SOURCE[0]}
1818
here=$(cd "$(dirname "$me")" && pwd)
1919
just_me=$(basename "$me")
2020

21-
: "${GEM_NAME:=sequel-pgt_outbox}"
21+
: "${GEM_NAME:=leopard}"
2222
: "${GIT_ORG:=rubyists}"
2323

2424
GEM_HOST=$1

examples/echo_endpoint.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative '../lib/leopard/nats_api_server'
5+
6+
# Example to echo the given message
7+
class EchoService
8+
include Rubyists::Leopard::NatsApiServer
9+
10+
endpoint(:echo) { |msg| Success(msg.data) }
11+
end
12+
13+
if __FILE__ == $PROGRAM_NAME
14+
EchoService.run(
15+
nats_url: 'nats://localhost:4222',
16+
service_opts: { name: 'example.echo', version: '1.0.0' },
17+
instances: 4,
18+
)
19+
end

leopard.gemspec

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
require_relative 'lib/leopard/version'
44

5-
Gem::Specification.new do |spec|
5+
Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
66
spec.name = 'leopard'
77
spec.version = Rubyists::Leopard::VERSION
88
spec.authors = ['bougyman']
@@ -34,7 +34,11 @@ Gem::Specification.new do |spec|
3434
spec.require_paths = ['lib']
3535

3636
# Uncomment to register a new dependency of your gem
37+
spec.add_dependency 'concurrent-ruby', '~> 1.1'
38+
spec.add_dependency 'dry-configurable', '~> 1.3'
39+
spec.add_dependency 'dry-monads', '~> 1.9'
3740
spec.add_dependency 'nats-pure', '~> 2.5'
41+
spec.add_dependency 'semantic_logger', '~> 4'
3842

3943
# For more information and examples about making a new gem, check out our
4044
# guide at: https://bundler.io/guides/creating_gem.html

lib/leopard.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
require 'dry/configurable'
4+
require 'pathname'
5+
require 'semantic_logger'
6+
SemanticLogger.add_appender(io: $stdout, formatter: :color)
7+
8+
class Pathname
9+
def /(other)
10+
join other.to_s
11+
end
12+
end
13+
14+
module Rubyists
15+
module Leopard
16+
end
17+
end
18+
19+
require_relative 'leopard/settings'
20+
require_relative 'leopard/version'
21+
require_relative 'leopard/errors'

0 commit comments

Comments
 (0)