use async_trait::async_trait;
use crate::asset::{AssetCapabilities, AssetMeta};
use crate::error::{Error, Result};
#[cfg(test)]
use crate::types::JobLogMode;
use crate::types::{
AddStructureGeneratorInput, AddStructureRowsInput, AssignToSprintInput, Comment,
CreateCommentInput, CreateIssueInput, CreateMergeRequestInput, CreatePageParams,
CreateStructureInput, CustomFieldDescriptor, Discussion, FileDiff, ForestModifyResult,
GetChatsParams, GetForestOptions, GetMessagesParams, GetPipelineInput, GetStructureValuesInput,
GetUsersOptions, Issue, IssueFilter, IssueRelations, IssueStatus, JobLogOptions, JobLogOutput,
KbPage, KbPageContent, KbSpace, ListCustomFieldsParams, ListPagesParams,
ListProjectVersionsParams, MeetingFilter, MeetingNote, MeetingTranscript, MergeRequest,
MessengerChat, MessengerMessage, MoveStructureRowsInput, MrFilter, PipelineInfo,
ProjectVersion, ProviderResult, Release, SaveStructureViewInput, SearchKbParams,
SearchMessagesParams, SendMessageParams, Sprint, SprintState, Structure, StructureForest,
StructureGenerator, StructureValues, StructureView, SyncStructureGeneratorInput,
UpdateIssueInput, UpdateMergeRequestInput, UpdatePageParams, UpdateStructureAutomationInput,
UpsertProjectVersionInput, User,
};
#[async_trait]
pub trait IssueProvider: Send + Sync {
async fn get_issues(&self, filter: IssueFilter) -> Result<ProviderResult<Issue>>;
async fn get_issue(&self, key: &str) -> Result<Issue>;
async fn create_issue(&self, input: CreateIssueInput) -> Result<Issue>;
async fn update_issue(&self, key: &str, input: UpdateIssueInput) -> Result<Issue>;
async fn get_comments(&self, issue_key: &str) -> Result<ProviderResult<Comment>>;
async fn add_comment(&self, issue_key: &str, body: &str) -> Result<Comment>;
async fn get_statuses(&self) -> Result<ProviderResult<IssueStatus>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_statuses".to_string(),
})
}
async fn link_issues(
&self,
_source_key: &str,
_target_key: &str,
_link_type: &str,
) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "link_issues".to_string(),
})
}
async fn unlink_issues(
&self,
_source_key: &str,
_target_key: &str,
_link_type: &str,
) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "unlink_issues".to_string(),
})
}
async fn get_users(&self, _options: GetUsersOptions) -> Result<ProviderResult<User>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_users".to_string(),
})
}
async fn upload_attachment(
&self,
_issue_key: &str,
_filename: &str,
_data: &[u8],
) -> Result<String> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "upload_attachment".to_string(),
})
}
async fn get_issue_attachments(&self, _issue_key: &str) -> Result<Vec<AssetMeta>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_issue_attachments".to_string(),
})
}
async fn download_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "download_attachment".to_string(),
})
}
async fn delete_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "delete_attachment".to_string(),
})
}
fn asset_capabilities(&self) -> AssetCapabilities {
AssetCapabilities::default()
}
async fn set_custom_fields(
&self,
_issue_key: &str,
_fields: &[serde_json::Value],
) -> Result<()> {
Ok(()) }
async fn get_issue_relations(&self, _issue_key: &str) -> Result<IssueRelations> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_issue_relations".to_string(),
})
}
async fn get_structures(&self) -> Result<ProviderResult<Structure>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_structures".to_string(),
})
}
async fn get_structure_forest(
&self,
_structure_id: u64,
_options: GetForestOptions,
) -> Result<StructureForest> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_structure_forest".to_string(),
})
}
async fn add_structure_rows(
&self,
_structure_id: u64,
_input: AddStructureRowsInput,
) -> Result<ForestModifyResult> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "add_structure_rows".to_string(),
})
}
async fn move_structure_rows(
&self,
_structure_id: u64,
_input: MoveStructureRowsInput,
) -> Result<ForestModifyResult> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "move_structure_rows".to_string(),
})
}
async fn remove_structure_row(&self, _structure_id: u64, _row_id: u64) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "remove_structure_row".to_string(),
})
}
async fn get_structure_values(
&self,
_input: GetStructureValuesInput,
) -> Result<StructureValues> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_structure_values".to_string(),
})
}
async fn get_structure_views(
&self,
_structure_id: u64,
_view_id: Option<u64>,
) -> Result<Vec<StructureView>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_structure_views".to_string(),
})
}
async fn save_structure_view(&self, _input: SaveStructureViewInput) -> Result<StructureView> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "save_structure_view".to_string(),
})
}
async fn create_structure(&self, _input: CreateStructureInput) -> Result<Structure> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "create_structure".to_string(),
})
}
async fn get_structure_generators(
&self,
_structure_id: u64,
) -> Result<ProviderResult<StructureGenerator>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_structure_generators".to_string(),
})
}
async fn add_structure_generator(
&self,
_input: AddStructureGeneratorInput,
) -> Result<StructureGenerator> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "add_structure_generator".to_string(),
})
}
async fn sync_structure_generator(&self, _input: SyncStructureGeneratorInput) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "sync_structure_generator".to_string(),
})
}
async fn delete_structure(&self, _structure_id: u64) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "delete_structure".to_string(),
})
}
async fn update_structure_automation(
&self,
_input: UpdateStructureAutomationInput,
) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "update_structure_automation".to_string(),
})
}
async fn trigger_structure_automation(&self, _structure_id: u64) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "trigger_structure_automation".to_string(),
})
}
async fn list_project_versions(
&self,
_params: ListProjectVersionsParams,
) -> Result<ProviderResult<ProjectVersion>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "list_project_versions".to_string(),
})
}
async fn upsert_project_version(
&self,
_input: UpsertProjectVersionInput,
) -> Result<ProjectVersion> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "upsert_project_version".to_string(),
})
}
async fn get_board_sprints(
&self,
_board_id: u64,
_state: SprintState,
) -> Result<ProviderResult<Sprint>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_board_sprints".to_string(),
})
}
async fn assign_to_sprint(&self, _input: AssignToSprintInput) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "assign_to_sprint".to_string(),
})
}
async fn list_custom_fields(
&self,
_params: ListCustomFieldsParams,
) -> Result<ProviderResult<CustomFieldDescriptor>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "list_custom_fields".to_string(),
})
}
fn provider_name(&self) -> &'static str;
}
#[async_trait]
pub trait UserProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_user_profile(&self, _user_id: &str) -> Result<User> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_user_profile".to_string(),
})
}
async fn lookup_user_by_email(&self, _email: &str) -> Result<Option<User>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "lookup_user_by_email".to_string(),
})
}
}
#[async_trait]
pub trait MergeRequestProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_merge_requests(&self, _filter: MrFilter) -> Result<ProviderResult<MergeRequest>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_merge_requests".to_string(),
})
}
async fn get_merge_request(&self, _key: &str) -> Result<MergeRequest> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_merge_request".to_string(),
})
}
async fn get_discussions(&self, _mr_key: &str) -> Result<ProviderResult<Discussion>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_discussions".to_string(),
})
}
async fn get_diffs(&self, _mr_key: &str) -> Result<ProviderResult<FileDiff>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_diffs".to_string(),
})
}
async fn add_comment(&self, _mr_key: &str, _input: CreateCommentInput) -> Result<Comment> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "add_merge_request_comment".to_string(),
})
}
async fn create_merge_request(&self, _input: CreateMergeRequestInput) -> Result<MergeRequest> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "create_merge_request".to_string(),
})
}
async fn update_merge_request(
&self,
_key: &str,
_input: UpdateMergeRequestInput,
) -> Result<MergeRequest> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "update_merge_request".to_string(),
})
}
async fn get_releases(&self) -> Result<ProviderResult<Release>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_releases".to_string(),
})
}
async fn get_mr_attachments(&self, _mr_key: &str) -> Result<Vec<AssetMeta>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_mr_attachments".to_string(),
})
}
async fn download_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "download_mr_attachment".to_string(),
})
}
async fn delete_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<()> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "delete_mr_attachment".to_string(),
})
}
}
#[async_trait]
pub trait PipelineProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_pipeline(&self, _input: GetPipelineInput) -> Result<PipelineInfo> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_pipeline".to_string(),
})
}
async fn get_job_logs(&self, _job_id: &str, _options: JobLogOptions) -> Result<JobLogOutput> {
Err(Error::ProviderUnsupported {
provider: self.provider_name().to_string(),
operation: "get_job_logs".to_string(),
})
}
}
#[async_trait]
pub trait Provider: IssueProvider + MergeRequestProvider + PipelineProvider {
async fn get_current_user(&self) -> Result<User>;
}
#[async_trait]
pub trait MeetingNotesProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_meetings(&self, filter: MeetingFilter) -> Result<ProviderResult<MeetingNote>>;
async fn get_transcript(&self, meeting_id: &str) -> Result<MeetingTranscript>;
async fn search_meetings(
&self,
query: &str,
filter: MeetingFilter,
) -> Result<ProviderResult<MeetingNote>>;
}
#[async_trait]
pub trait KnowledgeBaseProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_spaces(&self) -> Result<ProviderResult<KbSpace>>;
async fn list_pages(&self, params: ListPagesParams) -> Result<ProviderResult<KbPage>>;
async fn get_page(&self, page_id: &str) -> Result<KbPageContent>;
async fn create_page(&self, params: CreatePageParams) -> Result<KbPage>;
async fn update_page(&self, params: UpdatePageParams) -> Result<KbPage>;
async fn search(&self, params: SearchKbParams) -> Result<ProviderResult<KbPage>>;
}
#[async_trait]
pub trait MessengerProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
async fn get_chats(&self, params: GetChatsParams) -> Result<ProviderResult<MessengerChat>>;
async fn get_messages(
&self,
params: GetMessagesParams,
) -> Result<ProviderResult<MessengerMessage>>;
async fn search_messages(
&self,
params: SearchMessagesParams,
) -> Result<ProviderResult<MessengerMessage>>;
async fn send_message(&self, params: SendMessageParams) -> Result<MessengerMessage>;
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyProvider;
#[async_trait]
impl IssueProvider for DummyProvider {
async fn get_issues(&self, _: IssueFilter) -> Result<ProviderResult<Issue>> {
unreachable!("the dispatcher should never call this in these tests")
}
async fn get_issue(&self, _: &str) -> Result<Issue> {
unreachable!()
}
async fn create_issue(&self, _: CreateIssueInput) -> Result<Issue> {
unreachable!()
}
async fn update_issue(&self, _: &str, _: UpdateIssueInput) -> Result<Issue> {
unreachable!()
}
async fn get_comments(&self, _: &str) -> Result<ProviderResult<Comment>> {
unreachable!()
}
async fn add_comment(&self, _: &str, _: &str) -> Result<Comment> {
unreachable!()
}
fn provider_name(&self) -> &'static str {
"dummy"
}
}
#[async_trait]
impl MergeRequestProvider for DummyProvider {
fn provider_name(&self) -> &'static str {
"dummy"
}
}
#[async_trait]
impl PipelineProvider for DummyProvider {
fn provider_name(&self) -> &'static str {
"dummy"
}
}
fn assert_unsupported<T: std::fmt::Debug>(result: Result<T>, expected_op: &str) {
match result {
Err(Error::ProviderUnsupported {
provider,
operation,
}) => {
assert_eq!(provider, "dummy");
assert_eq!(operation, expected_op);
}
other => panic!("expected ProviderUnsupported({expected_op}), got {other:?}"),
}
}
#[tokio::test]
async fn issue_provider_defaults_return_unsupported() {
let p = DummyProvider;
assert_unsupported(p.get_statuses().await, "get_statuses");
assert_unsupported(p.link_issues("a", "b", "blocks").await, "link_issues");
assert_unsupported(p.unlink_issues("a", "b", "blocks").await, "unlink_issues");
assert_unsupported(p.get_users(GetUsersOptions::default()).await, "get_users");
assert_unsupported(
p.upload_attachment("k", "f.png", b"x").await,
"upload_attachment",
);
assert_unsupported(p.get_issue_attachments("k").await, "get_issue_attachments");
assert_unsupported(p.download_attachment("k", "1").await, "download_attachment");
assert_unsupported(p.delete_attachment("k", "1").await, "delete_attachment");
assert_unsupported(p.get_issue_relations("k").await, "get_issue_relations");
assert_unsupported(
p.list_project_versions(crate::types::ListProjectVersionsParams {
project: "PROJ".into(),
..Default::default()
})
.await,
"list_project_versions",
);
assert_unsupported(
p.upsert_project_version(crate::types::UpsertProjectVersionInput {
project: "PROJ".into(),
name: "1.0.0".into(),
..Default::default()
})
.await,
"upsert_project_version",
);
}
#[tokio::test]
async fn issue_provider_set_custom_fields_is_no_op_by_default() {
let p = DummyProvider;
p.set_custom_fields("k", &[]).await.unwrap();
}
#[test]
fn issue_provider_default_asset_capabilities_is_empty() {
let caps = IssueProvider::asset_capabilities(&DummyProvider);
assert_eq!(caps, AssetCapabilities::default());
}
#[tokio::test]
async fn merge_request_provider_defaults_return_unsupported() {
let p = DummyProvider;
assert_unsupported(
p.get_merge_requests(MrFilter::default()).await,
"get_merge_requests",
);
assert_unsupported(p.get_merge_request("mr#1").await, "get_merge_request");
assert_unsupported(p.get_discussions("mr#1").await, "get_discussions");
assert_unsupported(p.get_diffs("mr#1").await, "get_diffs");
assert_unsupported(
MergeRequestProvider::add_comment(
&p,
"mr#1",
CreateCommentInput {
body: "".into(),
position: None,
discussion_id: None,
},
)
.await,
"add_merge_request_comment",
);
assert_unsupported(
p.create_merge_request(CreateMergeRequestInput::default())
.await,
"create_merge_request",
);
assert_unsupported(
p.update_merge_request("mr#1", UpdateMergeRequestInput::default())
.await,
"update_merge_request",
);
assert_unsupported(p.get_releases().await, "get_releases");
assert_unsupported(p.get_mr_attachments("mr#1").await, "get_mr_attachments");
assert_unsupported(
p.download_mr_attachment("mr#1", "1").await,
"download_mr_attachment",
);
assert_unsupported(
p.delete_mr_attachment("mr#1", "1").await,
"delete_mr_attachment",
);
}
#[tokio::test]
async fn pipeline_provider_defaults_return_unsupported() {
let p = DummyProvider;
assert_unsupported(
p.get_pipeline(GetPipelineInput::default()).await,
"get_pipeline",
);
assert_unsupported(
p.get_job_logs(
"1",
JobLogOptions {
mode: JobLogMode::Smart,
},
)
.await,
"get_job_logs",
);
}
}