Skip to content

Repository files navigation

Ruuter-on-Rust

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 stub HttpResponse { status: 0, error: Some(kind), body: {error, message} } to result:, so a check_status switch can branch on it (#89); the stop_in_case_of_exception config 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-lint warns 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.

Try it in one command

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, requires RUUTER_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-rc

Prefer 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:rc

Every published digest is signed keyless via cosign — verify with the recipe in book/src/ops/docker.md.

Lint / test your DSL tree in CI (issue #83)

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.

Build from source

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 --build

To wipe and rebuild after code changes:

docker compose down
docker compose up -d --build --force-recreate

How it works

A DSL file at DSL/<project>/<METHOD>/<path>.yml becomes the route <METHOD> /<project>/<path>. Example:

# DSL/samples/GET/ping.yml
response:
  status: 202
  return: pong

Reachable at GET /samples/ping.

Step types

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.

Guards

<stem>.guard.yml next to a directory protects every DSL under it. A guard returning status >= 400 short-circuits the request.

WebSocket server

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.

WebSocket sources (consume upstream)

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.

Configuration

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.

Constants and secrets

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.

Config file resolution

At boot Ruuter looks for a YAML config file in this priority:

  1. --config <path> CLI flag.
  2. RUUTER_CONFIG=<path> env var.
  3. ./ruuter.yaml or ./ruuter.yml in the working directory.
  4. 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.

Observability

OpenTelemetry OTLP export is opt-in:

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_SERVICE_NAME=ruuter-on-rust \
docker compose up -d --build

W3C traceparent is adopted or generated on every request and echoed back with X-Trace-Id; outbound HTTP calls forward it automatically.

Admin endpoint

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.

Buerostack integration

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.

Documentation

About

Ruuter-on-Rust — declarative REST + WebSocket router. Rust rewrite of Buerokratt Ruuter.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages