use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GitLabScope {
Project {
id: String,
},
Group {
id: String,
},
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GitHubScope {
Repository {
owner: String,
repo: String,
},
Organization {
name: String,
},
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClickUpScope {
List {
id: String,
team_id: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JiraScope {
Project {
key: String,
},
MultiProject { keys: Vec<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConfluenceScope {
Space {
key: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SlackScope {
Workspace { team_id: Option<String> },
}
#[derive(Debug, Clone)]
pub enum ConfluenceAuthConfig {
BearerToken {
token: SecretString,
},
Basic {
username: String,
password: SecretString,
},
}
#[derive(Debug, Clone)]
pub enum ProviderConfig {
GitLab {
base_url: String,
access_token: SecretString,
scope: GitLabScope,
extra: HashMap<String, serde_json::Value>,
},
GitHub {
base_url: String,
access_token: SecretString,
scope: GitHubScope,
extra: HashMap<String, serde_json::Value>,
},
ClickUp {
access_token: SecretString,
scope: ClickUpScope,
extra: HashMap<String, serde_json::Value>,
},
Jira {
base_url: String,
access_token: SecretString,
email: String,
scope: JiraScope,
flavor: Option<devboy_jira::JiraFlavor>,
extra: HashMap<String, serde_json::Value>,
},
Confluence {
base_url: String,
auth: ConfluenceAuthConfig,
scope: ConfluenceScope,
api_version: Option<String>,
extra: HashMap<String, serde_json::Value>,
},
Fireflies {
api_key: SecretString,
extra: HashMap<String, serde_json::Value>,
},
Slack {
base_url: String,
access_token: SecretString,
scope: SlackScope,
required_scopes: Vec<String>,
extra: HashMap<String, serde_json::Value>,
},
Custom {
name: String,
config: HashMap<String, serde_json::Value>,
},
}
impl ProviderConfig {
pub fn provider_name(&self) -> &str {
match self {
Self::GitLab { .. } => "gitlab",
Self::GitHub { .. } => "github",
Self::ClickUp { .. } => "clickup",
Self::Jira { .. } => "jira",
Self::Confluence { .. } => "confluence",
Self::Fireflies { .. } => "fireflies",
Self::Slack { .. } => "slack",
Self::Custom { name, .. } => name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
pub url: String,
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderMetadata {
pub data: serde_json::Value,
}
impl ProviderMetadata {
pub fn new(data: serde_json::Value) -> Self {
Self { data }
}
}
#[derive(Debug, Clone)]
pub struct AdditionalContext {
pub provider: ProviderConfig,
pub proxy: Option<ProxyConfig>,
pub metadata: Option<ProviderMetadata>,
pub extra: HashMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use secrecy::ExposeSecret;
fn token(s: &str) -> SecretString {
SecretString::from(s.to_string())
}
#[test]
fn test_provider_config_gitlab_project_scope() {
let config = ProviderConfig::GitLab {
base_url: "https://gitlab.com".into(),
access_token: token("glpat-xxx"),
scope: GitLabScope::Project { id: "12345".into() },
extra: HashMap::new(),
};
assert_eq!(config.provider_name(), "gitlab");
}
#[test]
fn test_provider_config_github_repo_scope() {
let config = ProviderConfig::GitHub {
base_url: "https://api.github.com".into(),
access_token: token("ghp_xxx"),
scope: GitHubScope::Repository {
owner: "meteora-pro".into(),
repo: "devboy-tools".into(),
},
extra: HashMap::new(),
};
assert_eq!(config.provider_name(), "github");
}
#[test]
fn test_provider_config_custom() {
let config = ProviderConfig::Custom {
name: "my-provider".into(),
config: HashMap::new(),
};
assert_eq!(config.provider_name(), "my-provider");
}
#[test]
fn test_provider_config_confluence_scope() {
let config = ProviderConfig::Confluence {
base_url: "https://wiki.example.com".into(),
auth: ConfluenceAuthConfig::BearerToken {
token: token("pat-token"),
},
scope: ConfluenceScope::Space {
key: Some("ENG".into()),
},
api_version: Some("v1".into()),
extra: HashMap::new(),
};
assert_eq!(config.provider_name(), "confluence");
}
#[test]
fn test_provider_name_clickup() {
let config = ProviderConfig::ClickUp {
access_token: token("pk_test"),
scope: ClickUpScope::List {
id: "list1".into(),
team_id: None,
},
extra: HashMap::new(),
};
assert_eq!(config.provider_name(), "clickup");
}
#[test]
fn test_provider_name_jira() {
let config = ProviderConfig::Jira {
base_url: "https://jira.example.com".into(),
access_token: token("tok"),
email: "a@b.com".into(),
scope: JiraScope::Project { key: "X".into() },
flavor: None,
extra: HashMap::new(),
};
assert_eq!(config.provider_name(), "jira");
}
#[test]
fn test_provider_metadata_new() {
let data = serde_json::json!({"statuses": [{"name": "Done"}]});
let meta = ProviderMetadata::new(data.clone());
assert_eq!(meta.data, data);
}
#[test]
fn test_proxy_config_serialize_deserialize() {
let mut headers = HashMap::new();
headers.insert("X-Routing".into(), "internal".into());
let proxy = ProxyConfig {
url: "https://proxy.internal/jira".into(),
headers,
};
let json = serde_json::to_string(&proxy).unwrap();
let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.url, "https://proxy.internal/jira");
assert_eq!(deserialized.headers["X-Routing"], "internal");
}
#[test]
fn test_provider_config_debug_redacts_access_token() {
let config = ProviderConfig::GitLab {
base_url: "https://gitlab.com".into(),
access_token: token("super-secret-glpat"),
scope: GitLabScope::Project { id: "12345".into() },
extra: HashMap::new(),
};
let dbg = format!("{:?}", config);
assert!(
!dbg.contains("super-secret-glpat"),
"Debug must redact access_token, got: {dbg}"
);
}
#[test]
fn test_confluence_auth_basic_password_redacted() {
let auth = ConfluenceAuthConfig::Basic {
username: "dev@example.com".into(),
password: token("super-secret-password"),
};
let dbg = format!("{:?}", auth);
assert!(
!dbg.contains("super-secret-password"),
"Basic password must not appear in Debug: {dbg}"
);
if let ConfluenceAuthConfig::Basic { password, .. } = &auth {
assert_eq!(password.expose_secret(), "super-secret-password");
}
}
}