1use scrybe_rpc::{default_socket_path, JsonRpcVersion, Request, Response};
12use std::io::{BufRead, BufReader, Write};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15
16#[cfg(unix)]
17use std::os::unix::net::UnixStream;
18
19const READ_TIMEOUT: Duration = Duration::from_secs(2);
22
23pub fn socket_path() -> PathBuf {
25 default_socket_path()
26}
27
28#[cfg(unix)]
32pub fn try_connect() -> Result<Option<UnixStream>, String> {
33 try_connect_at(&default_socket_path())
34}
35
36#[cfg(unix)]
39pub fn try_connect_at(path: &Path) -> Result<Option<UnixStream>, String> {
40 if !path.exists() {
41 return Ok(None);
42 }
43 match UnixStream::connect(path) {
44 Ok(s) => {
45 s.set_read_timeout(Some(READ_TIMEOUT))
46 .map_err(|e| format!("set_read_timeout: {e}"))?;
47 Ok(Some(s))
48 }
49 Err(e)
50 if matches!(
51 e.kind(),
52 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
53 ) =>
54 {
55 Ok(None)
56 }
57 Err(e) => Err(format!("connect to {}: {e}", path.display())),
58 }
59}
60
61#[cfg(not(unix))]
62pub fn try_connect() -> Result<Option<()>, String> {
63 Ok(None)
64}
65
66#[cfg(not(unix))]
67pub fn try_connect_at(_path: &Path) -> Result<Option<()>, String> {
68 Ok(None)
69}
70
71pub fn send(method: &str, params: serde_json::Value) -> Result<Response, String> {
73 send_to(&default_socket_path(), method, params)
74}
75
76#[cfg(unix)]
78pub fn send_to(socket: &Path, method: &str, params: serde_json::Value) -> Result<Response, String> {
79 let mut stream = match try_connect_at(socket)? {
80 Some(s) => s,
81 None => return Err("no Scrybe running".to_string()),
82 };
83 let req = Request {
84 jsonrpc: JsonRpcVersion,
85 id: 1,
86 method: method.to_string(),
87 params,
88 };
89 let line = serde_json::to_string(&req).map_err(|e| format!("serialize: {e}"))?;
90 writeln!(stream, "{line}").map_err(|e| format!("write: {e}"))?;
91 let mut reader = BufReader::new(stream);
92 let mut response_line = String::new();
93 reader
94 .read_line(&mut response_line)
95 .map_err(|e| format!("read: {e}"))?;
96 if response_line.is_empty() {
97 return Err("server closed connection without responding".to_string());
98 }
99 let resp: Response = serde_json::from_str(response_line.trim_end())
100 .map_err(|e| format!("parse response: {e}"))?;
101 Ok(resp)
102}
103
104#[cfg(not(unix))]
105pub fn send_to(
106 _socket: &Path,
107 _method: &str,
108 _params: serde_json::Value,
109) -> Result<Response, String> {
110 Err("scrybe-cli RPC is unix-only in Phase 1".to_string())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use scrybe_rpc::ERR_METHOD_NOT_FOUND;
117
118 #[test]
119 fn socket_path_exposed() {
120 let p = socket_path();
121 assert!(!p.as_os_str().is_empty());
122 }
123
124 #[test]
125 fn request_serializes_to_single_line() {
126 let req = Request {
127 jsonrpc: JsonRpcVersion,
128 id: 1,
129 method: "open".into(),
130 params: serde_json::json!({"path": "/tmp/foo.md"}),
131 };
132 let s = serde_json::to_string(&req).unwrap();
133 assert!(!s.contains('\n'));
134 }
135
136 #[test]
137 fn response_with_error_parses() {
138 let line = format!(
139 r#"{{"jsonrpc":"2.0","id":1,"error":{{"code":{ERR_METHOD_NOT_FOUND},"message":"x"}}}}"#,
140 );
141 let r: Response = serde_json::from_str(&line).unwrap();
142 assert!(r.result.is_none());
143 assert_eq!(r.error.unwrap().code, ERR_METHOD_NOT_FOUND);
144 }
145
146 #[test]
147 fn try_connect_at_returns_none_when_no_socket() {
148 let path = std::path::PathBuf::from("/tmp/scrybe-nonexistent-sock-unit-test");
149 let result = try_connect_at(&path).unwrap();
150 assert!(result.is_none());
151 }
152
153 #[test]
154 fn send_to_errors_when_no_server() {
155 let path = std::path::PathBuf::from("/tmp/scrybe-nonexistent-sock-send-unit-test");
156 let err = send_to(&path, "open", serde_json::json!({"path": "/tmp/foo.md"})).unwrap_err();
157 assert!(err.contains("no Scrybe running"));
158 }
159}