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