Rust implementation of Ruuter — a declarative REST/WebSocket router driven by YAML DSLs on disk.
Version: 0.9.15-rc (pre-release; v1.0.0 is the next stable target) · License: Apache-2.0 · Author: Rainer Türner
Upgrading from v0.9.14-rc? Three fixes and a docs/tooling round on top of v0.9.14-rc:
http.*transport failures (connection-refused, DNS, TLS, timeout) are now catchable by the DSL — the step binds a stubHttpResponse { status: 0, error: Some(kind), body: {error, message} }toresult:, so acheck_statusswitch can branch on it (#89); thestop_in_case_of_exceptionconfig field no longer WARNs on every boot for operators who never touched it (#92); the supported JavaScript subset is documented and empirically verified on both Boa and QuickJS (#90);dsl-lintwarns on five classes of YAML plain-scalar hazard::inside an unquoted${…},#inside one,,inside one in flow context, values starting with reserved metasyntax (!,&,*,%,@, backtick), and Unicode homoglyphs in structural YAML (#91). Details in CHANGELOG.md § 0.9.15-rc.
Multi-arch image (linux/amd64 + linux/arm64) on Docker Hub and GHCR:
docker run -d --name ruuter -p 8080:8080 \
turnerrainer/ruuter:0.9.15-rc- Health check:
curl http://localhost:8080/health→{"status":"ok"}. - Sample route:
curl http://localhost:8080/samples/ping→"pong". - OpenAPI spec (auto-generated from every DSL):
curl http://localhost:8080/_/openapi.json— admin-gated, requiresRUUTER_ADMIN_ENABLED=true.
The image bakes in DSL/samples/ so every endpoint under /samples/*
works out of the box. Mount your own tree to override:
docker run -d --name ruuter -p 8080:8080 \
-v $(pwd)/DSL:/app/DSL:ro \
-v $(pwd)/constants.ini:/app/constants.ini:ro \
turnerrainer/ruuter:0.9.15-rcPrefer a shorter pull recipe? While we're on release candidates,
:rc always points at the latest RC — it moves each time a new
-rc.N publishes, and it never touches :latest:
docker pull turnerrainer/ruuter:rcEvery published digest is signed keyless via cosign — verify with the recipe in book/src/ops/docker.md.
dsl-lint (static analysis) and dsl-test (runtime scenarios) ship
inside the same image as the runtime binary, at exactly the engine
version they'll be validating against:
# Lint every DSL under ./DSL against constants.ini.
docker run --rm -v "$PWD:/w" -w /w \
turnerrainer/ruuter:0.9.15-rc \
dsl-lint --dsl DSL --constants constants.ini
# Run every DSL-test scenario under ./DSL-tests.
docker run --rm -v "$PWD:/w" -w /w \
turnerrainer/ruuter:0.9.15-rc \
dsl-test --dsl DSL --tests DSL-tests --constants constants.ini:rc for tracking the latest RC in CI; pin to :0.9.13-rc in a
release-branch CI so a downstream job doesn't silently upgrade
tooling mid-flight.
For hacking on Ruuter itself:
git clone -b dev https://github.com/turnerrainer/Ruuter.git ruuter-on-rust
cd ruuter-on-rust
docker compose up -d --buildTo wipe and rebuild after code changes:
docker compose down
docker compose up -d --build --force-recreateA DSL file at DSL/<project>/<METHOD>/<path>.yml becomes the route
<METHOD> /<project>/<path>. Example:
# DSL/samples/GET/ping.yml
response:
status: 202
return: pongReachable at GET /samples/ping.
assign, return, http (http.get/post/put/patch/delete),
switch, log, state, iterate, ws_send, template.
See DSL/samples/README.md for worked examples of each.
<stem>.guard.yml next to a directory protects every DSL under it.
A guard returning status >= 400 short-circuits the request.
Drop DSL/<project>/WS/<path>.yml and clients connect at
ws://localhost:8080/<project>/<path>. The DSL runs once per inbound
frame with incoming.body, incoming.connection_id, incoming.headers,
incoming.params. Reply via ws_send.
Configure at DSL/<project>/sources/<name>.yml; each inbound frame
dispatches to DSL/<project>/triggers/<channel>/<key>.yml (with
_default.yml as fallback). See
DSL/samples/sources/stock-feed.yml.disabled.
Layout:
DSL/— routes/guards/triggers/sources/WS DSLs (mounted read-only).constants.ini—[#KEY]values referenced from DSLs (mounted RO).ruuter.yaml— operator config file (optional, see below).docker-compose.yml— deployment; container is hardened (read_only,no-new-privileges,cap_drop: ALL, mem/cpu limits).- Environment:
RUST_LOG=info|debug|warn|error.
DSLs reference [#KEY] values from a constants.ini file mounted
into the container (read-only). Section headers ([DSL], etc.) are
accepted for Java-Ruuter compatibility but do not scope keys — every
KEY=value line is flat. Comments start with #. Missing keys
referenced from a WS source config error at load time; missing keys
in a DSL body are substituted as literal [#KEY] (visible at runtime).
Secrets management is out of scope. Ruuter reads constants from a
file — it does NOT fetch from Vault, KMS, Docker secrets, or any
external store. Mount the resolved secrets file at
/app/constants.ini (or bind a Vault-agent-rendered file over it).
Rotation, sourcing, and access control are the deployment pipeline's
job, not the framework's.
At boot Ruuter looks for a YAML config file in this priority:
--config <path>CLI flag.RUUTER_CONFIG=<path>env var../ruuter.yamlor./ruuter.ymlin the working directory.- Built-in defaults if none of the above exists.
A worked example with every top-level knob lives at
DSL/samples/ruuter.yaml.example — copy to ./ruuter.yaml and edit.
The full config surface (CORS, CSRF Origin allow-list, Idempotency-Key
cache, SSRF allow-list, response-size cap, method allow-list, Boa
runtime limits, etc.) is documented in src/config/mod.rs. Every
setting has a safe default; only override what you need.
OpenTelemetry OTLP export is opt-in:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_SERVICE_NAME=ruuter-on-rust \
docker compose up -d --buildW3C traceparent is adopted or generated on every request and echoed
back with X-Trace-Id; outbound HTTP calls forward it automatically.
GET /_/sources reports the source supervisor's health.
GET /_/unguarded reports which routes are guarded vs unguarded
(HTTP + WS; issue #45).
GET /_/openapi.json returns the auto-generated OpenAPI 3.1 spec.
All three off by default; enable with RUUTER_ADMIN_ENABLED=true.
Ruuter owns HTTP routing, WebSocket endpoints, event-trigger dispatch, ephemeral in-process state, and pre-execution guards. It does not own: scheduled work (CronManager), persistent storage (Resql), identity/JWT (TIM), inter-service payload shaping (DataMapper).
For CronManager → Ruuter scheduled jobs, define the endpoint in Ruuter
(DSL/<project>/POST/scheduled/<job>.yml) and a matching HTTP job in
CronManager. Protect production endpoints with a guard verifying a
shared secret. Worked sample:
DSL/samples/POST/scheduled/heartbeat.yml +
DSL/samples/cronmanager-jobs/heartbeat.yaml.
- Book (mdBook) — full LLM-oriented reference. Build locally with
mdbook serve book; browses at http://localhost:3000. Auto-deployed to GitHub Pages on push tomain(see.github/workflows/docs.yml). - DSL reference (single page) — same content, single Markdown file.
- CHANGELOG.md
- CLAUDE.md — brief for coding agents: release-gate commands, breaking-change surface, best-practice config matrix, grep recipes for finding risky settings.
- Development TODO
- Original Java Ruuter: https://github.com/buerokratt/Ruuter