use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use std::path::PathBuf;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ApiConnectionInfo {
pub port: u16,
pub token: String,
}
fn api_info_path() -> PathBuf {
tmai_core::ipc::protocol::state_dir().join("api.json")
}
pub fn write_api_info(port: u16, token: &str) -> Result<()> {
let dir = tmai_core::ipc::protocol::state_dir();
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create state dir: {}", dir.display()))?;
let info = ApiConnectionInfo {
port,
token: token.to_string(),
};
let path = api_info_path();
let json = serde_json::to_string(&info)?;
std::fs::write(&path, &json)
.with_context(|| format!("Failed to write API info: {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
pub fn remove_api_info() {
let _ = std::fs::remove_file(api_info_path());
}
#[derive(Debug, Clone)]
pub struct TmaiHttpClient {
base_url: String,
token: String,
}
impl TmaiHttpClient {
pub fn from_runtime() -> Result<Self> {
let path = api_info_path();
let data = std::fs::read_to_string(&path)
.with_context(|| format!("tmai is not running (no API info at {})", path.display()))?;
let info: ApiConnectionInfo =
serde_json::from_str(&data).context("Invalid API info file")?;
Ok(Self {
base_url: format!("http://localhost:{}", info.port),
token: info.token,
})
}
pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}/api{}", self.base_url, path);
let resp: T = ureq::get(&url)
.header("Authorization", &format!("Bearer {}", self.token))
.call()
.with_context(|| format!("GET {path} failed"))?
.body_mut()
.read_json()
.with_context(|| format!("Failed to parse response from {path}"))?;
Ok(resp)
}
pub fn post<T: DeserializeOwned>(&self, path: &str, body: &serde_json::Value) -> Result<T> {
let url = format!("{}/api{}", self.base_url, path);
let resp: T = ureq::post(&url)
.header("Authorization", &format!("Bearer {}", self.token))
.send_json(body)
.with_context(|| format!("POST {path} failed"))?
.body_mut()
.read_json()
.with_context(|| format!("Failed to parse response from {path}"))?;
Ok(resp)
}
pub fn post_ok(&self, path: &str, body: &serde_json::Value) -> Result<()> {
let url = format!("{}/api{}", self.base_url, path);
ureq::post(&url)
.header("Authorization", &format!("Bearer {}", self.token))
.send_json(body)
.with_context(|| format!("POST {path} failed"))?;
Ok(())
}
pub fn resolve_repo(&self, repo: &Option<String>) -> Result<String> {
if let Some(r) = repo {
return Ok(r.clone());
}
if let Ok(cwd) = std::env::current_dir() {
let cwd_str = cwd.to_string_lossy().to_string();
if cwd.join(".git").exists() {
return Ok(cwd_str);
}
}
let projects: Vec<String> = self.get("/projects")?;
projects
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No registered projects. Specify repo explicitly."))
}
pub fn get_text(&self, path: &str) -> Result<String> {
let url = format!("{}/api{}", self.base_url, path);
let text = ureq::get(&url)
.header("Authorization", &format!("Bearer {}", self.token))
.call()
.with_context(|| format!("GET {path} failed"))?
.body_mut()
.read_to_string()
.with_context(|| format!("Failed to read response from {path}"))?;
Ok(text)
}
}