use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use serde_json::Value;
use crate::daemon::server;
#[derive(Parser)]
pub struct ServiceCommand {
#[arg(value_name = "SERVICE")]
pub service: String,
#[arg(value_name = "OP")]
pub op: String,
#[arg(long, value_name = "JSON")]
pub payload: Option<String>,
#[arg(long, value_name = "PATH")]
pub socket: Option<PathBuf>,
}
impl ServiceCommand {
pub async fn execute(self) -> Result<()> {
let payload = parse_payload(self.payload.as_deref())?;
let socket = server::resolve_socket(self.socket)?;
let reply = super::call_service(&socket, &self.service, &self.op, payload).await?;
println!("{}", serde_json::to_string_pretty(&reply)?);
Ok(())
}
}
fn parse_payload(raw: Option<&str>) -> Result<Value> {
match raw {
Some(raw) => serde_json::from_str(raw)
.with_context(|| format!("`--payload` is not valid JSON: {raw}")),
None => Ok(Value::Null),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn parse_payload_defaults_to_null_and_parses_json() {
assert_eq!(parse_payload(None).unwrap(), Value::Null);
assert_eq!(
parse_payload(Some(r#"{"id":3}"#)).unwrap(),
serde_json::json!({ "id": 3 })
);
assert!(parse_payload(Some("{not json")).is_err());
}
#[test]
fn parses_service_op_and_flags() {
let cmd = ServiceCommand::try_parse_from([
"service",
"browser-bridge",
"disconnect-tab",
"--payload",
r#"{"id":3}"#,
"--socket",
"/tmp/d.sock",
])
.unwrap();
assert_eq!(cmd.service, "browser-bridge");
assert_eq!(cmd.op, "disconnect-tab");
assert_eq!(cmd.payload.as_deref(), Some(r#"{"id":3}"#));
assert_eq!(
cmd.socket.as_deref(),
Some(std::path::Path::new("/tmp/d.sock"))
);
assert!(ServiceCommand::try_parse_from(["service", "browser-bridge"]).is_err());
}
#[tokio::test]
async fn execute_sends_the_op_and_prints_the_reply() {
let (_dir, sock, server) = crate::daemon::testutil::fake_daemon_reply(
serde_json::json!({ "ok": true, "payload": { "connected": false } }),
);
let cmd = ServiceCommand {
service: "browser-bridge".to_string(),
op: "status".to_string(),
payload: None,
socket: Some(sock),
};
cmd.execute().await.unwrap();
server.await.unwrap();
}
#[tokio::test]
async fn execute_rejects_malformed_payload_before_touching_the_socket() {
let cmd = ServiceCommand {
service: "browser-bridge".to_string(),
op: "disconnect-tab".to_string(),
payload: Some("{not json".to_string()),
socket: Some(PathBuf::from("/nonexistent/omni-dev-svc.sock")),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not valid JSON"), "{err}");
}
}