1pub mod anthropic;
11pub mod elevenlabs;
12pub mod gtts;
13
14use std::io::Read;
15use std::time::Duration;
16
17#[derive(Debug, thiserror::Error)]
19pub enum ApiError {
20 #[error("status_code: {status}, body: {body}")]
22 Status { status: u16, body: String },
23 #[error("{0}")]
25 Transport(String),
26}
27
28impl ApiError {
29 pub fn status(&self) -> Option<u16> {
30 match self {
31 ApiError::Status { status, .. } => Some(*status),
32 ApiError::Transport(_) => None,
33 }
34 }
35}
36
37pub(crate) fn agent() -> ureq::Agent {
40 static AGENT: std::sync::OnceLock<ureq::Agent> = std::sync::OnceLock::new();
41 AGENT
42 .get_or_init(|| {
43 ureq::AgentBuilder::new()
44 .timeout_connect(Duration::from_secs(30))
45 .timeout_read(Duration::from_secs(240))
46 .build()
47 })
48 .clone()
49}
50
51fn log_response(method: &str, resp: &ureq::Response) {
54 xil_core::log::info(&format!(
55 "HTTP Request: {method} {} \"{} {} {}\"",
56 resp.get_url(),
57 resp.http_version(),
58 resp.status(),
59 resp.status_text()
60 ));
61}
62
63pub(crate) fn call_logged(
66 method: &str,
67 result: Result<ureq::Response, ureq::Error>,
68) -> Result<ureq::Response, ApiError> {
69 match result {
70 Ok(r) => {
71 log_response(method, &r);
72 Ok(r)
73 }
74 Err(ureq::Error::Status(status, resp)) => {
75 log_response(method, &resp);
76 Err(ApiError::Status {
77 status,
78 body: resp.into_string().unwrap_or_default(),
79 })
80 }
81 Err(ureq::Error::Transport(t)) => Err(ApiError::Transport(t.to_string())),
82 }
83}
84
85pub(crate) fn call(
87 result: Result<ureq::Response, ureq::Error>,
88) -> Result<ureq::Response, ApiError> {
89 match result {
90 Ok(r) => Ok(r),
91 Err(ureq::Error::Status(status, resp)) => Err(ApiError::Status {
92 status,
93 body: resp.into_string().unwrap_or_default(),
94 }),
95 Err(ureq::Error::Transport(t)) => Err(ApiError::Transport(t.to_string())),
96 }
97}
98
99#[allow(clippy::result_large_err)]
102pub(crate) fn send_json(
103 req: ureq::Request,
104 body: &serde_json::Value,
105) -> Result<ureq::Response, ureq::Error> {
106 req.set("Content-Type", "application/json")
107 .send_string(&serde_json::to_string(body).unwrap_or_default())
108}
109
110pub(crate) fn read_bytes(resp: ureq::Response) -> Result<Vec<u8>, ApiError> {
111 let mut buf = Vec::new();
112 resp.into_reader()
113 .read_to_end(&mut buf)
114 .map_err(|e| ApiError::Transport(e.to_string()))?;
115 Ok(buf)
116}
117
118pub(crate) fn read_json(resp: ureq::Response) -> Result<serde_json::Value, ApiError> {
119 let bytes = read_bytes(resp)?;
120 serde_json::from_slice(&bytes)
121 .map_err(|e| ApiError::Transport(format!("invalid JSON response: {e}")))
122}
123
124pub(crate) fn base_url(env: &str, default: &str) -> String {
126 std::env::var(env)
127 .ok()
128 .filter(|v| !v.is_empty())
129 .unwrap_or_else(|| default.to_string())
130 .trim_end_matches('/')
131 .to_string()
132}