basilisk-rust-client 0.1.1

Rust client for the basilisk reverse proxy server
Documentation
# rust-client

Rust client library for Milestone Basilisk gateway and service bus.

## Features

- Register a service instance, receive a generated instance ID, and establish an authenticated service bus connection in one workflow
- Subscribe/unsubscribe to topics and receive events
- Publish events with structured payloads
- Send forward requests and wait for forward responses
- Register request responders with `client.on_request(message_type, responder)`
- Call gateway registry APIs to register/deregister service instances
- Use `BasiliskClient` as the top-level client with `gateway` and `bus` handles

## API reference

This section documents the public APIs that implement the features above.

### Top-level client (`BasiliskClient`)

Use this when you want a single workflow that registers in the gateway and then opens an authenticated bus connection.

`BasiliskClientConfig` fields:

- `gateway_base_url`: Gateway base URL (for example `http://127.0.0.1:3000`)
- `bus_host`: TCP host for the service bus
- `bus_port`: TCP port for the service bus
- `service_id`: Logical service identifier
- `fingerprint`: Service fingerprint/version marker
- `path_prefixes`: Path prefixes advertised to the gateway
- `scheme`: Upstream scheme (`http` or `https`)
- `host`: Upstream host
- `port`: Upstream port
- `weight`: Instance load-balancing weight
- `registration_auth_type`: Gateway auth type (for example `token`)
- `registration_token`: Gateway registration token/secret

`BasiliskClient` public fields:

- `gateway: GatewayApiClient`
- `bus: BusClient`
- `service_id: String`
- `instance_id: String`

`BasiliskClient` methods:

- `connect(config) -> anyhow::Result<BasiliskClient>`
  - Registers first (`/registry/register`) and then authenticates the bus connection.
- `deregister() -> anyhow::Result<()>`
  - Calls gateway deregistration for the current `service_id` + `instance_id`.
- `subscribe(topics) -> ClientResult<()>`
- `unsubscribe(topics) -> ClientResult<()>`
- `publish(topic, message_type, payload) -> ClientResult<i32>`
  - Returns subscriber count from the bus `ack` frame.
- `publish_event(event) -> ClientResult<i32>`
- `forward(request) -> ClientResult<ServiceBusForwardResponse>`
- `on_event(topic, handler) -> ClientResult<()>`
  - Registers an async event callback and auto-subscribes to that topic.
- `on_request(message_type, responder) -> ClientResult<()>`
  - Registers an async request responder keyed by message type.

### Service bus client (`BusClient`)

Use this when you need lower-level direct control over the TCP bus.

`BusClient` methods:

- `connect(host, port, service_id, instance_id, token) -> ClientResult<BusClient>`
  - First protocol frame is `connect` carrying auth credentials.
  - Frames are newline-delimited JSON (`\n` terminated).
- `subscribe(topics) -> ClientResult<()>`
- `unsubscribe(topics) -> ClientResult<()>`
- `publish(topic, message_type, payload) -> ClientResult<i32>`
- `publish_event(event) -> ClientResult<i32>`
- `forward(request) -> ClientResult<ServiceBusForwardResponse>`
- `on_event(topic, handler) -> ClientResult<()>`
- `on_request(message_type, responder) -> ClientResult<()>`

Supporting bus API types:

- `ForwardRequest`
  - `target_service_id`, `message_type`, `payload`, `timeout_ms`
- `ServiceBusRequest`
  - `event: ServiceBusEventEnvelope`
  - `reply_to() -> Option<&str>`
- `RequestResponder`
  - `respond(message_type, payload) -> ClientResult<i32>`
  - `respond_ok(payload) -> ClientResult<i32>`

### Gateway API client (`GatewayApiClient`)

Use this when you only need registry operations.

`GatewayApiClient` methods:

- `new(base_url) -> GatewayApiClient`
- `register_instance_auto(request) -> anyhow::Result<RegistrationResponse>`
  - Clears `request.instance.instance_id` and lets the registry generate it.
- `register_instance(request) -> anyhow::Result<RegistrationResponse>`
- `deregister_instance(service_id, instance_id) -> anyhow::Result<()>`

Gateway payload types:

- `RegistrationRequest`
  - `service_id`, `fingerprint`, `path_prefixes`, `instance`, `auth`
- `InstanceInfo`
  - `instance_id`, `scheme`, `host`, `port`, `weight`
- `AuthInfo`
  - `auth_type`, `token`
- `RegistrationResponse`
  - `message`, `service_id`, `instance_id`, `token`

### Protocol types

These mirror wire payloads used over the service bus.

- `ServiceBusEventEnvelope`
  - `event_id`, `emitted_at_utc`, `service_id`, `instance_id`, `topic`, `message_type`, `correlation_id`, `causation_id`, `payload`
- `ServiceBusForwardRequest`
- `ServiceBusForwardResponse`
- `ServiceBusProtocolMessage`
  - Generic frame container for `connect`, `subscribe`, `publish`, `forward`, `event`, `ack`, and `error` frames.
- `protocol_types`
  - String constants for protocol frame names.

### Error model

- `ClientResult<T> = Result<T, ClientError>`
- `ClientError`
  - `Io`, `Serde`, `Protocol { code, message }`, `MissingField`, `ChannelClosed`, `UnexpectedMessage`

All fallible bus operations return `ClientResult<T>` and preserve protocol/transport details.

## Quick usage

```rust
use rust_client::{BasiliskClient, BasiliskClientConfig, ForwardRequest, init_tracing};
use std::collections::HashMap;

async fn run() -> anyhow::Result<()> {
    init_tracing();
    let client = BasiliskClient::connect(BasiliskClientConfig {
        gateway_base_url: "http://127.0.0.1:3000".to_string(),
        bus_host: "127.0.0.1".to_string(),
        bus_port: 5090,
        service_id: "orders".to_string(),
        fingerprint: "orders-v1".to_string(),
        path_prefixes: vec!["/api/orders".to_string()],
        scheme: "http".to_string(),
        host: "127.0.0.1".to_string(),
        port: 7001,
        weight: 1,
        registration_auth_type: "token".to_string(),
        registration_token: "replace-me".to_string(),
    })
    .await?;

    tracing::info!(instance_id = %client.instance_id, "Connected to Basilisk");

    client
        .on_request("order.query", |_request, responder| async move {
            let mut payload = HashMap::new();
            payload.insert("orderId".to_string(), serde_json::json!("42"));
            responder.respond_ok(payload).await?;
            Ok(())
        })
        .await?;

    let response = client
        .forward(ForwardRequest {
            target_service_id: "orders".to_string(),
            message_type: "order.query".to_string(),
            payload: HashMap::new(),
            timeout_ms: Some(2_000),
        })
        .await?;

    tracing::info!(message_type = %response.message_type, "Forward response received");
    Ok(())
}
```

`BasiliskClient` owns a `gateway` client for registry operations and a `bus` client for
the TCP service bus. The top-level `connect` flow automatically registers the instance,
accepts the generated instance ID returned by the registry, and then opens the bus
connection using the issued token.

## Logging

Call `init_tracing()` once during application startup to install Basilisk's
console subscriber. It reads [`RUST_LOG`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
for log filtering and defaults to `INFO` when the variable is absent. If your
application has already installed a `tracing` subscriber, do not call it.

```bash
RUST_LOG=debug cargo run --example basic_service_bus
```

## Notes on low-level clients

- `BusClient::connect(host, port, service_id, instance_id, token)` now authenticates during
  the `connect` handshake (no separate authenticate command is required for normal clients).
- `GatewayApiClient::register_instance(...)` responses include both `instance_id` and `token`.
- `GatewayApiClient::register_instance_auto(...)` sends an empty instance ID so the registry
  generates a cryptographically strong instance identity.

## End-to-end test

The integration test starts:

- a temporary Basilisk gateway instance,
- an upstream HTTP service,
- two connected Basilisk clients (`orders` and `billing`).

It validates registry registration, proxy forwarding and Lua middleware header forwarding, publish/subscribe, and forward-request responder behavior.

Run:

```bash
cargo test -- --nocapture
```