use clap::Args as ClapArgs;
use futures_util::StreamExt;
use reqwest::Client;
use std::time::Duration;
use tokio_tungstenite::tungstenite::Message;
use url::Url;
#[derive(Debug, Clone, ClapArgs)]
pub struct TailArgs {
#[arg(long, default_value_t = true)]
pub follow: bool,
#[arg(long, default_value_t = 100)]
pub limit: usize,
#[arg(long)]
pub doc: Option<String>,
}
pub async fn run(args: TailArgs, server: &Url) -> i32 {
let client = match Client::builder().timeout(Duration::from_secs(5)).build() {
Ok(c) => c,
Err(e) => {
eprintln!("client init: {e}");
return 1;
}
};
let mut url = match server.join("/api/events") {
Ok(u) => u,
Err(e) => {
eprintln!("invalid server URL: {e}");
return 2;
}
};
{
let mut qp = url.query_pairs_mut();
qp.append_pair("limit", &args.limit.to_string());
if let Some(d) = &args.doc {
qp.append_pair("doc", d);
}
}
let initial: serde_json::Value = match client.get(url).send().await {
Ok(r) if r.status().is_success() => match r.json().await {
Ok(v) => v,
Err(e) => {
eprintln!("replay parse: {e}");
return 1;
}
},
Ok(r) => {
eprintln!("replay returned {}", r.status());
return 2;
}
Err(e) => {
eprintln!("replay failed: {e}");
return 1;
}
};
if let Some(events) = initial.get("events").and_then(|v| v.as_array()) {
for ev in events {
if let Some(doc) = &args.doc {
if ev.get("doc").and_then(|d| d.as_str()) != Some(doc.as_str()) {
continue;
}
}
println!("{ev}");
}
}
let next_since = initial
.get("nextSince")
.and_then(|v| v.as_str())
.map(str::to_string);
if !args.follow {
return 0;
}
let ws_url = {
let mut u = server.clone();
let scheme = match u.scheme() {
"http" => "ws",
"https" => "wss",
other => {
eprintln!("unsupported scheme: {other}");
return 1;
}
};
u.set_scheme(scheme).expect("set_scheme to ws/wss");
u.set_path("/ws/events");
u
};
let (mut ws, _resp) = match tokio_tungstenite::connect_async(ws_url.as_str()).await {
Ok(c) => c,
Err(e) => {
eprintln!("ws connect: {e}");
return 1;
}
};
let sigint = tokio::signal::ctrl_c();
tokio::pin!(sigint);
loop {
tokio::select! {
_ = &mut sigint => break,
msg = ws.next() => match msg {
Some(Ok(Message::Text(t))) => {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&t) {
if v.get("type").and_then(|x| x.as_str()) == Some("event") {
if let Some(ev) = v.get("event") {
if let Some(doc) = &args.doc {
if ev.get("doc").and_then(|d| d.as_str())
!= Some(doc.as_str())
{
continue;
}
}
println!("{ev}");
}
}
}
}
Some(Ok(Message::Close(_))) | None => break,
Some(Ok(_)) => continue,
Some(Err(e)) => {
eprintln!("ws recv: {e}");
return 2;
}
}
}
}
let _ = ws.close(None).await;
let _ = next_since; 0
}
#[cfg(test)]
mod tests {
use url::Url;
#[test]
fn scheme_swap_http_to_ws() {
let mut u: Url = Url::parse("http://127.0.0.1:4173").unwrap();
u.set_scheme("ws").unwrap();
u.set_path("/ws/events");
assert_eq!(u.scheme(), "ws");
assert_eq!(u.host_str(), Some("127.0.0.1"));
assert_eq!(u.port(), Some(4173));
assert_eq!(u.path(), "/ws/events");
}
#[test]
fn scheme_swap_https_to_wss() {
let mut u: Url = Url::parse("https://example.com:9000").unwrap();
u.set_scheme("wss").unwrap();
assert_eq!(u.scheme(), "wss");
}
}