use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{Result, anyhow};
use crate::tickets::api::backends::{
Backend, github::GitHubBackend, jira::JiraBackend, linear::LinearBackend,
};
use crate::tickets::api::config::{BackendConfig, Config};
pub struct BackendClient {
backends: HashMap<String, Arc<dyn Backend>>,
default_backend: Option<String>,
}
impl BackendClient {
pub async fn from_config(config: Config) -> Result<Self> {
let mut backends: HashMap<String, Arc<dyn Backend>> = HashMap::new();
for (name, bc) in config.backends.into_iter() {
let result: Result<Arc<dyn Backend>> = match bc {
BackendConfig::Github(c) => GitHubBackend::new(c).map(|b| Arc::new(b) as _),
BackendConfig::Jira(c) => JiraBackend::new(c).map(|b| Arc::new(b) as _),
BackendConfig::Linear(c) => LinearBackend::new(c).map(|b| Arc::new(b) as _),
};
match result {
Ok(b) => {
backends.insert(name, b);
}
Err(e) => {
tracing::warn!("backend '{name}' could not be initialised: {e}");
}
}
}
Ok(Self {
backends,
default_backend: config.default_backend,
})
}
pub fn resolve(&self, requested: Option<&str>) -> Result<Arc<dyn Backend>> {
let key = requested
.map(|s| s.to_string())
.or_else(|| self.default_backend.clone())
.or_else(|| {
if self.backends.len() == 1 {
self.backends.keys().next().cloned()
} else {
None
}
})
.ok_or_else(|| {
anyhow!(
"no backend specified and no default configured (configured: {:?})",
self.list_backends()
)
})?;
self.backends
.get(&key)
.cloned()
.ok_or_else(|| anyhow!("backend '{key}' not configured"))
}
pub fn list_backends(&self) -> Vec<String> {
let mut v: Vec<String> = self.backends.keys().cloned().collect();
v.sort();
v
}
pub fn default_backend(&self) -> Option<&str> {
self.default_backend.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn from_config_constructs_empty() {
let cfg = Config::default();
let client = BackendClient::from_config(cfg).await.unwrap();
assert!(client.list_backends().is_empty());
assert!(client.resolve(None).is_err());
}
#[tokio::test(flavor = "current_thread")]
async fn list_backends_lists_configured() {
let mut backends: HashMap<String, Arc<dyn Backend>> = HashMap::new();
backends.insert("github".into(), Arc::new(FakeBackend("github")));
backends.insert("linear".into(), Arc::new(FakeBackend("linear")));
let client = BackendClient {
backends,
default_backend: Some("github".into()),
};
let names = client.list_backends();
assert_eq!(names, vec!["github".to_string(), "linear".to_string()]);
}
#[tokio::test(flavor = "current_thread")]
async fn resolve_picks_default() {
let mut backends: HashMap<String, Arc<dyn Backend>> = HashMap::new();
backends.insert("github".into(), Arc::new(FakeBackend("github")));
backends.insert("linear".into(), Arc::new(FakeBackend("linear")));
let client = BackendClient {
backends,
default_backend: Some("linear".into()),
};
let b = client.resolve(None).unwrap();
assert_eq!(b.name(), "linear");
let b = client.resolve(Some("github")).unwrap();
assert_eq!(b.name(), "github");
}
struct FakeBackend(&'static str);
#[async_trait::async_trait]
impl Backend for FakeBackend {
fn name(&self) -> &'static str {
self.0
}
async fn create_issue(
&self,
_p: crate::tickets::api::backends::CreateIssueParams,
) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn get_issue(&self, _id: &str) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn update_issue(
&self,
_id: &str,
_p: crate::tickets::api::backends::UpdateIssueParams,
) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn close_issue(
&self,
_id: &str,
_c: Option<&str>,
) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn reopen_issue(&self, _id: &str) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn list_issues(
&self,
_p: crate::tickets::api::backends::ListIssuesParams,
) -> Result<Vec<crate::tickets::api::models::Issue>> {
unimplemented!()
}
async fn search_issues(
&self,
_p: crate::tickets::api::backends::SearchIssuesParams,
) -> Result<Vec<crate::tickets::api::models::Issue>> {
unimplemented!()
}
async fn add_comment(
&self,
_i: &str,
_b: &str,
) -> Result<crate::tickets::api::models::Comment> {
unimplemented!()
}
async fn list_comments(
&self,
_i: &str,
) -> Result<Vec<crate::tickets::api::models::Comment>> {
unimplemented!()
}
async fn update_comment(
&self,
_i: &str,
_c: &str,
_b: &str,
) -> Result<crate::tickets::api::models::Comment> {
unimplemented!()
}
async fn delete_comment(&self, _i: &str, _c: &str) -> Result<()> {
unimplemented!()
}
async fn list_labels(&self) -> Result<Vec<crate::tickets::api::models::Label>> {
unimplemented!()
}
async fn create_label(
&self,
_n: &str,
_c: Option<&str>,
_d: Option<&str>,
) -> Result<crate::tickets::api::models::Label> {
unimplemented!()
}
async fn add_labels(&self, _i: &str, _l: &[String]) -> Result<()> {
unimplemented!()
}
async fn remove_labels(&self, _i: &str, _l: &[String]) -> Result<()> {
unimplemented!()
}
async fn list_milestones(&self) -> Result<Vec<crate::tickets::api::models::Milestone>> {
unimplemented!()
}
async fn create_milestone(
&self,
_p: crate::tickets::api::backends::CreateMilestoneParams,
) -> Result<crate::tickets::api::models::Milestone> {
unimplemented!()
}
async fn close_milestone(
&self,
_id: &str,
) -> Result<crate::tickets::api::models::Milestone> {
unimplemented!()
}
async fn get_milestone_issues(
&self,
_id: &str,
) -> Result<Vec<crate::tickets::api::models::Issue>> {
unimplemented!()
}
async fn list_projects(&self) -> Result<Vec<crate::tickets::api::models::Project>> {
unimplemented!()
}
async fn get_project(&self, _id: &str) -> Result<crate::tickets::api::models::Project> {
unimplemented!()
}
async fn list_epics(&self) -> Result<Vec<crate::tickets::api::models::Issue>> {
unimplemented!()
}
async fn get_epic_issues(
&self,
_id: &str,
) -> Result<Vec<crate::tickets::api::models::Issue>> {
unimplemented!()
}
async fn create_project_update(
&self,
_p: &str,
_b: &str,
_h: Option<&str>,
) -> Result<crate::tickets::api::models::ProjectUpdate> {
unimplemented!()
}
async fn list_project_updates(
&self,
_p: &str,
) -> Result<Vec<crate::tickets::api::models::ProjectUpdate>> {
unimplemented!()
}
async fn list_states(&self) -> Result<Vec<String>> {
unimplemented!()
}
async fn transition_issue(
&self,
_id: &str,
_s: &str,
) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
async fn assign_issue(
&self,
_id: &str,
_a: &str,
) -> Result<crate::tickets::api::models::Issue> {
unimplemented!()
}
}
}