use crate::error::{CalDirError, CalDirResult};
use crate::remote::protocol::{
Command, Connect, ConnectResponse, ProviderCommand, Request, Response,
};
use crate::remote::provider_account::ProviderAccount;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::process::Command as TokioCommand;
use tokio::time::timeout;
const PROVIDER_TIMEOUT: Duration = Duration::from_secs(15);
const AUTH_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Provider(String);
impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Provider {
pub fn from_name(name: &str) -> Self {
Provider(name.to_string())
}
pub fn name(&self) -> &str {
&self.0
}
fn provider_search_dirs() -> impl Iterator<Item = std::path::PathBuf> {
let provider_path = std::env::var_os("CALDIR_PROVIDER_PATH");
let system_path = std::env::var_os("PATH");
provider_path
.into_iter()
.flat_map(|p| std::env::split_paths(&p).collect::<Vec<_>>())
.chain(
system_path
.into_iter()
.flat_map(|p| std::env::split_paths(&p).collect::<Vec<_>>()),
)
}
pub fn discover_installed() -> Vec<String> {
let mut providers = std::collections::BTreeSet::new();
let prefix = "caldir-provider-";
for dir in Self::provider_search_dirs() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if let Some(provider_name) = name.strip_prefix(prefix) {
providers.insert(provider_name.to_string());
}
}
}
providers.into_iter().collect()
}
fn binary_path(&self) -> CalDirResult<std::path::PathBuf> {
let binary_name = format!("caldir-provider-{}", self.0);
if let Some(provider_path) = std::env::var_os("CALDIR_PROVIDER_PATH") {
for dir in std::env::split_paths(&provider_path) {
let candidate = dir.join(&binary_name);
if candidate.is_file() {
return Ok(candidate);
}
}
}
let binary_path = which::which(&binary_name).map_err(|_| {
CalDirError::ProviderNotInstalled(format!(
"Provider '{}' not found. Install it with:\n cargo install {}",
self.0, binary_name
))
})?;
Ok(binary_path)
}
pub async fn connect(
&self,
options: serde_json::Map<String, serde_json::Value>,
data: serde_json::Map<String, serde_json::Value>,
) -> CalDirResult<ConnectResponse> {
self.call_no_timeout(Connect { options, data }).await
}
pub fn provider_account(&self, identifier: String) -> ProviderAccount {
ProviderAccount::new(self.clone(), identifier)
}
pub async fn call<C: ProviderCommand>(&self, cmd: C) -> CalDirResult<C::Response> {
timeout(PROVIDER_TIMEOUT, self.call_raw(C::command(), cmd))
.await
.map_err(|_| CalDirError::ProviderTimeout(PROVIDER_TIMEOUT.as_secs()))?
}
pub async fn call_no_timeout<C: ProviderCommand>(&self, cmd: C) -> CalDirResult<C::Response> {
timeout(AUTH_TIMEOUT, self.call_raw(C::command(), cmd))
.await
.map_err(|_| CalDirError::ProviderTimeout(AUTH_TIMEOUT.as_secs()))?
}
async fn call_raw<P: Serialize, R: serde::de::DeserializeOwned>(
&self,
command: Command,
params: P,
) -> CalDirResult<R> {
let params =
serde_json::to_value(params).map_err(|e| CalDirError::Serialization(e.to_string()))?;
let request = Request { command, params };
let request_json = serde_json::to_string(&request)
.map_err(|e| CalDirError::Serialization(e.to_string()))?;
let binary_path = self.binary_path()?;
let mut child = TokioCommand::new(&binary_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| {
CalDirError::Provider(format!("Failed to spawn {}: {}", binary_path.display(), e))
})?;
let mut stdin = child.stdin.take().unwrap();
stdin
.write_all(format!("{request_json}\n").as_bytes())
.await?;
drop(stdin);
let output = child.wait_with_output().await?;
if !output.status.success() {
return Err(CalDirError::Provider(format!(
"Provider exited with status: {}",
output.status.code().unwrap_or(-1)
)));
}
let response_str = String::from_utf8_lossy(&output.stdout);
if response_str.is_empty() {
return Err(CalDirError::Provider(
"Provider returned no response".into(),
));
}
let response: Response<R> = serde_json::from_str(&response_str)
.map_err(|e| CalDirError::Provider(format!("Failed to parse response: {}", e)))?;
match response {
Response::Success { data } => Ok(data),
Response::Error { error } => Err(CalDirError::Provider(error)),
}
}
}