1use anyhow::{Context, Result};
18use serde_json::{Value, json};
19
20pub struct BbtRpcClient {
25 client: reqwest::blocking::Client,
26 url: String,
27 next_id: std::cell::Cell<u64>,
28}
29
30impl BbtRpcClient {
31 pub fn new(url: &str) -> Self {
32 Self {
33 client: reqwest::blocking::Client::builder()
34 .timeout(std::time::Duration::from_secs(30))
35 .build()
36 .expect("Failed to build HTTP client"),
37 url: url.into(),
38 next_id: std::cell::Cell::new(1),
39 }
40 }
41
42 fn call(&self, method: &str, params: Value) -> Result<Value> {
44 let id = self.next_id.get();
45 self.next_id.set(id + 1);
46
47 let payload = json!({
48 "jsonrpc": "2.0",
49 "method": method,
50 "params": params,
51 "id": id,
52 });
53
54 let resp = self
55 .client
56 .post(&self.url)
57 .json(&payload)
58 .send()
59 .with_context(|| {
60 "Cannot connect to Zotero. Ensure Zotero is running with Better BibTeX installed."
61 })?;
62
63 let body: Value = resp
64 .json()
65 .with_context(|| "Invalid JSON response from BBT")?;
66
67 if let Some(error) = body.get("error") {
68 let msg = error
69 .get("message")
70 .and_then(|m| m.as_str())
71 .unwrap_or("Unknown BBT error");
72 anyhow::bail!("BBT RPC error: {msg}");
73 }
74
75 Ok(body.get("result").cloned().unwrap_or(Value::Null))
76 }
77
78 pub fn export(&self, citekeys: &[&str], translator: &str) -> Result<String> {
82 let result = self.call("item.export", json!([citekeys, translator]))?;
83 match result {
84 Value::String(s) => Ok(s),
85 _ => Ok(result.to_string()),
86 }
87 }
88
89 pub fn bibliography(&self, citekeys: &[&str], style: &str) -> Result<String> {
93 let result = self.call("item.bibliography", json!([citekeys, {"id": style}]))?;
94 match result {
95 Value::String(s) => Ok(s),
96 _ => Ok(result.to_string()),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn client_creates_with_default_url() {
107 let client = BbtRpcClient::new("http://localhost:23119/better-bibtex/json-rpc");
108 assert_eq!(client.url, "http://localhost:23119/better-bibtex/json-rpc");
109 }
110
111 #[test]
112 fn call_to_unreachable_server_returns_error() {
113 let client = BbtRpcClient::new("http://127.0.0.1:1/nonexistent");
114 let result = client.export(&["test2024"], "Better BibTeX");
115 assert!(result.is_err());
116 let err = result.unwrap_err().to_string();
117 assert!(
118 err.contains("Cannot connect") || err.contains("error"),
119 "Unexpected error: {err}"
120 );
121 }
122}