sausage-plugin part 2: import! with typed clients (sausage_interface::client_rs), TCP endpoints with rpc.endpoint, examples

Part 1 fixes: complete stdout writes, buffered stdin, Rust type names
in schemas, enum variant docs, -32603 for serde errors in handlers,
crate docs and modules. End-to-end check of a generated consumer through
a fake core.
This commit is contained in:
2026-10-23 00:35:45 +01:00
parent ae1cb4cc99
commit 8d3c163992
42 changed files with 3020 additions and 856 deletions
+75
View File
@@ -0,0 +1,75 @@
//! Serve files from `/state/www` and expose a control endpoint at `/self/control`.
use sausage_plugin::{Ctx, Result, background, listen, pid, rpc, schema, state_dir};
use std::{cell::Cell, rc::Rc};
use wstd::{
io::{AsyncRead, AsyncWrite},
iter::AsyncIterator,
net::TcpListener,
};
struct Www;
/// The server configuration.
#[schema]
struct StartParams {
/// The HTTP port on all network interfaces.
port: u16,
}
#[rpc(endpoints(control=Control),grace_seconds=5)]
impl Www {
/// Start the HTTP server and return its process ID.
async fn start(&self, p: StartParams) -> Result<String> {
let tcp = TcpListener::bind(&format!("0.0.0.0:{}", p.port)).await?;
let control = Rc::new(Control { hits: Cell::new(0) });
listen("/self/control", control.clone()).await?;
background(serve(tcp, control));
Ok(pid())
}
}
struct Control {
hits: Cell<u64>,
}
#[rpc]
impl Control {
/// Count the HTTP requests this server answered.
fn stats(&self) -> Result<u64> {
Ok(self.hits.get())
}
/// End this process after the response reaches the client.
fn stop(&self, ctx: &Ctx) -> Result<()> {
ctx.exit_after_response();
Ok(())
}
}
async fn serve(tcp: TcpListener, control: Rc<Control>) {
let mut incoming = tcp.incoming();
while let Some(Ok(mut stream)) = incoming.next().await {
let mut request = [0; 8192];
let Ok(size) = stream.read(&mut request).await else {
continue;
};
let line = String::from_utf8_lossy(&request[..size]);
let path = line
.split_whitespace()
.nth(1)
.unwrap_or("/")
.trim_start_matches('/');
let (status, body) = match std::fs::read(state_dir().join(if path.is_empty() {
"index.html"
} else {
path
})) {
Ok(body) => ("200 OK", body),
Err(_) => ("404 Not Found", b"Not found".to_vec()),
};
control.hits.set(control.hits.get() + 1);
let header = format!(
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = stream.write_all(header.as_bytes()).await;
let _ = stream.write_all(&body).await;
let _ = stream.flush().await;
}
}
fn main() {
sausage_plugin::run(Www)
}