1use async_trait::async_trait;
7
8use crate::asset::{AssetCapabilities, AssetMeta};
9use crate::error::{Error, Result};
10#[cfg(test)]
11use crate::types::JobLogMode;
12use crate::types::{
13 AddStructureGeneratorInput, AddStructureRowsInput, AssignToSprintInput, Comment,
14 CreateCommentInput, CreateIssueInput, CreateMergeRequestInput, CreatePageParams,
15 CreateStructureInput, Discussion, FileDiff, ForestModifyResult, GetChatsParams,
16 GetForestOptions, GetMessagesParams, GetPipelineInput, GetStructureValuesInput,
17 GetUsersOptions, Issue, IssueFilter, IssueRelations, IssueStatus, JobLogOptions, JobLogOutput,
18 KbPage, KbPageContent, KbSpace, ListPagesParams, ListProjectVersionsParams, MeetingFilter,
19 MeetingNote, MeetingTranscript, MergeRequest, MessengerChat, MessengerMessage,
20 MoveStructureRowsInput, MrFilter, PipelineInfo, ProjectVersion, ProviderResult, Release,
21 SaveStructureViewInput, SearchKbParams, SearchMessagesParams, SendMessageParams, Sprint,
22 SprintState, Structure, StructureForest, StructureGenerator, StructureValues, StructureView,
23 SyncStructureGeneratorInput, UpdateIssueInput, UpdateMergeRequestInput, UpdatePageParams,
24 UpdateStructureAutomationInput, UpsertProjectVersionInput, User,
25};
26
27#[async_trait]
31pub trait IssueProvider: Send + Sync {
32 async fn get_issues(&self, filter: IssueFilter) -> Result<ProviderResult<Issue>>;
34
35 async fn get_issue(&self, key: &str) -> Result<Issue>;
37
38 async fn create_issue(&self, input: CreateIssueInput) -> Result<Issue>;
40
41 async fn update_issue(&self, key: &str, input: UpdateIssueInput) -> Result<Issue>;
43
44 async fn get_comments(&self, issue_key: &str) -> Result<ProviderResult<Comment>>;
45
46 async fn add_comment(&self, issue_key: &str, body: &str) -> Result<Comment>;
47
48 async fn get_statuses(&self) -> Result<ProviderResult<IssueStatus>> {
51 Err(Error::ProviderUnsupported {
52 provider: self.provider_name().to_string(),
53 operation: "get_statuses".to_string(),
54 })
55 }
56
57 async fn link_issues(
59 &self,
60 _source_key: &str,
61 _target_key: &str,
62 _link_type: &str,
63 ) -> Result<()> {
64 Err(Error::ProviderUnsupported {
65 provider: self.provider_name().to_string(),
66 operation: "link_issues".to_string(),
67 })
68 }
69
70 async fn unlink_issues(
72 &self,
73 _source_key: &str,
74 _target_key: &str,
75 _link_type: &str,
76 ) -> Result<()> {
77 Err(Error::ProviderUnsupported {
78 provider: self.provider_name().to_string(),
79 operation: "unlink_issues".to_string(),
80 })
81 }
82
83 async fn get_users(&self, _options: GetUsersOptions) -> Result<ProviderResult<User>> {
85 Err(Error::ProviderUnsupported {
86 provider: self.provider_name().to_string(),
87 operation: "get_users".to_string(),
88 })
89 }
90
91 async fn upload_attachment(
94 &self,
95 _issue_key: &str,
96 _filename: &str,
97 _data: &[u8],
98 ) -> Result<String> {
99 Err(Error::ProviderUnsupported {
100 provider: self.provider_name().to_string(),
101 operation: "upload_attachment".to_string(),
102 })
103 }
104
105 async fn get_issue_attachments(&self, _issue_key: &str) -> Result<Vec<AssetMeta>> {
111 Err(Error::ProviderUnsupported {
112 provider: self.provider_name().to_string(),
113 operation: "get_issue_attachments".to_string(),
114 })
115 }
116
117 async fn download_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
123 Err(Error::ProviderUnsupported {
124 provider: self.provider_name().to_string(),
125 operation: "download_attachment".to_string(),
126 })
127 }
128
129 async fn delete_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<()> {
136 Err(Error::ProviderUnsupported {
137 provider: self.provider_name().to_string(),
138 operation: "delete_attachment".to_string(),
139 })
140 }
141
142 fn asset_capabilities(&self) -> AssetCapabilities {
147 AssetCapabilities::default()
148 }
149
150 async fn set_custom_fields(
153 &self,
154 _issue_key: &str,
155 _fields: &[serde_json::Value],
156 ) -> Result<()> {
157 Ok(()) }
159
160 async fn get_issue_relations(&self, _issue_key: &str) -> Result<IssueRelations> {
162 Err(Error::ProviderUnsupported {
163 provider: self.provider_name().to_string(),
164 operation: "get_issue_relations".to_string(),
165 })
166 }
167
168 async fn get_structures(&self) -> Result<ProviderResult<Structure>> {
173 Err(Error::ProviderUnsupported {
174 provider: self.provider_name().to_string(),
175 operation: "get_structures".to_string(),
176 })
177 }
178
179 async fn get_structure_forest(
181 &self,
182 _structure_id: u64,
183 _options: GetForestOptions,
184 ) -> Result<StructureForest> {
185 Err(Error::ProviderUnsupported {
186 provider: self.provider_name().to_string(),
187 operation: "get_structure_forest".to_string(),
188 })
189 }
190
191 async fn add_structure_rows(
193 &self,
194 _structure_id: u64,
195 _input: AddStructureRowsInput,
196 ) -> Result<ForestModifyResult> {
197 Err(Error::ProviderUnsupported {
198 provider: self.provider_name().to_string(),
199 operation: "add_structure_rows".to_string(),
200 })
201 }
202
203 async fn move_structure_rows(
205 &self,
206 _structure_id: u64,
207 _input: MoveStructureRowsInput,
208 ) -> Result<ForestModifyResult> {
209 Err(Error::ProviderUnsupported {
210 provider: self.provider_name().to_string(),
211 operation: "move_structure_rows".to_string(),
212 })
213 }
214
215 async fn remove_structure_row(&self, _structure_id: u64, _row_id: u64) -> Result<()> {
217 Err(Error::ProviderUnsupported {
218 provider: self.provider_name().to_string(),
219 operation: "remove_structure_row".to_string(),
220 })
221 }
222
223 async fn get_structure_values(
225 &self,
226 _input: GetStructureValuesInput,
227 ) -> Result<StructureValues> {
228 Err(Error::ProviderUnsupported {
229 provider: self.provider_name().to_string(),
230 operation: "get_structure_values".to_string(),
231 })
232 }
233
234 async fn get_structure_views(
236 &self,
237 _structure_id: u64,
238 _view_id: Option<u64>,
239 ) -> Result<Vec<StructureView>> {
240 Err(Error::ProviderUnsupported {
241 provider: self.provider_name().to_string(),
242 operation: "get_structure_views".to_string(),
243 })
244 }
245
246 async fn save_structure_view(&self, _input: SaveStructureViewInput) -> Result<StructureView> {
248 Err(Error::ProviderUnsupported {
249 provider: self.provider_name().to_string(),
250 operation: "save_structure_view".to_string(),
251 })
252 }
253
254 async fn create_structure(&self, _input: CreateStructureInput) -> Result<Structure> {
256 Err(Error::ProviderUnsupported {
257 provider: self.provider_name().to_string(),
258 operation: "create_structure".to_string(),
259 })
260 }
261
262 async fn get_structure_generators(
266 &self,
267 _structure_id: u64,
268 ) -> Result<ProviderResult<StructureGenerator>> {
269 Err(Error::ProviderUnsupported {
270 provider: self.provider_name().to_string(),
271 operation: "get_structure_generators".to_string(),
272 })
273 }
274
275 async fn add_structure_generator(
277 &self,
278 _input: AddStructureGeneratorInput,
279 ) -> Result<StructureGenerator> {
280 Err(Error::ProviderUnsupported {
281 provider: self.provider_name().to_string(),
282 operation: "add_structure_generator".to_string(),
283 })
284 }
285
286 async fn sync_structure_generator(&self, _input: SyncStructureGeneratorInput) -> Result<()> {
288 Err(Error::ProviderUnsupported {
289 provider: self.provider_name().to_string(),
290 operation: "sync_structure_generator".to_string(),
291 })
292 }
293
294 async fn delete_structure(&self, _structure_id: u64) -> Result<()> {
298 Err(Error::ProviderUnsupported {
299 provider: self.provider_name().to_string(),
300 operation: "delete_structure".to_string(),
301 })
302 }
303
304 async fn update_structure_automation(
306 &self,
307 _input: UpdateStructureAutomationInput,
308 ) -> Result<()> {
309 Err(Error::ProviderUnsupported {
310 provider: self.provider_name().to_string(),
311 operation: "update_structure_automation".to_string(),
312 })
313 }
314
315 async fn trigger_structure_automation(&self, _structure_id: u64) -> Result<()> {
317 Err(Error::ProviderUnsupported {
318 provider: self.provider_name().to_string(),
319 operation: "trigger_structure_automation".to_string(),
320 })
321 }
322
323 async fn list_project_versions(
333 &self,
334 _params: ListProjectVersionsParams,
335 ) -> Result<ProviderResult<ProjectVersion>> {
336 Err(Error::ProviderUnsupported {
337 provider: self.provider_name().to_string(),
338 operation: "list_project_versions".to_string(),
339 })
340 }
341
342 async fn upsert_project_version(
346 &self,
347 _input: UpsertProjectVersionInput,
348 ) -> Result<ProjectVersion> {
349 Err(Error::ProviderUnsupported {
350 provider: self.provider_name().to_string(),
351 operation: "upsert_project_version".to_string(),
352 })
353 }
354
355 async fn get_board_sprints(
359 &self,
360 _board_id: u64,
361 _state: SprintState,
362 ) -> Result<ProviderResult<Sprint>> {
363 Err(Error::ProviderUnsupported {
364 provider: self.provider_name().to_string(),
365 operation: "get_board_sprints".to_string(),
366 })
367 }
368
369 async fn assign_to_sprint(&self, _input: AssignToSprintInput) -> Result<()> {
371 Err(Error::ProviderUnsupported {
372 provider: self.provider_name().to_string(),
373 operation: "assign_to_sprint".to_string(),
374 })
375 }
376
377 fn provider_name(&self) -> &'static str;
379}
380
381#[async_trait]
394pub trait UserProvider: Send + Sync {
395 fn provider_name(&self) -> &'static str;
397
398 async fn get_user_profile(&self, _user_id: &str) -> Result<User> {
401 Err(Error::ProviderUnsupported {
402 provider: self.provider_name().to_string(),
403 operation: "get_user_profile".to_string(),
404 })
405 }
406
407 async fn lookup_user_by_email(&self, _email: &str) -> Result<Option<User>> {
411 Err(Error::ProviderUnsupported {
412 provider: self.provider_name().to_string(),
413 operation: "lookup_user_by_email".to_string(),
414 })
415 }
416}
417
418#[async_trait]
424pub trait MergeRequestProvider: Send + Sync {
425 fn provider_name(&self) -> &'static str;
427
428 async fn get_merge_requests(&self, _filter: MrFilter) -> Result<ProviderResult<MergeRequest>> {
430 Err(Error::ProviderUnsupported {
431 provider: self.provider_name().to_string(),
432 operation: "get_merge_requests".to_string(),
433 })
434 }
435
436 async fn get_merge_request(&self, _key: &str) -> Result<MergeRequest> {
438 Err(Error::ProviderUnsupported {
439 provider: self.provider_name().to_string(),
440 operation: "get_merge_request".to_string(),
441 })
442 }
443
444 async fn get_discussions(&self, _mr_key: &str) -> Result<ProviderResult<Discussion>> {
446 Err(Error::ProviderUnsupported {
447 provider: self.provider_name().to_string(),
448 operation: "get_discussions".to_string(),
449 })
450 }
451
452 async fn get_diffs(&self, _mr_key: &str) -> Result<ProviderResult<FileDiff>> {
453 Err(Error::ProviderUnsupported {
454 provider: self.provider_name().to_string(),
455 operation: "get_diffs".to_string(),
456 })
457 }
458
459 async fn add_comment(&self, _mr_key: &str, _input: CreateCommentInput) -> Result<Comment> {
460 Err(Error::ProviderUnsupported {
461 provider: self.provider_name().to_string(),
462 operation: "add_merge_request_comment".to_string(),
463 })
464 }
465
466 async fn create_merge_request(&self, _input: CreateMergeRequestInput) -> Result<MergeRequest> {
468 Err(Error::ProviderUnsupported {
469 provider: self.provider_name().to_string(),
470 operation: "create_merge_request".to_string(),
471 })
472 }
473
474 async fn update_merge_request(
476 &self,
477 _key: &str,
478 _input: UpdateMergeRequestInput,
479 ) -> Result<MergeRequest> {
480 Err(Error::ProviderUnsupported {
481 provider: self.provider_name().to_string(),
482 operation: "update_merge_request".to_string(),
483 })
484 }
485
486 async fn get_releases(&self) -> Result<ProviderResult<Release>> {
488 Err(Error::ProviderUnsupported {
489 provider: self.provider_name().to_string(),
490 operation: "get_releases".to_string(),
491 })
492 }
493
494 async fn get_mr_attachments(&self, _mr_key: &str) -> Result<Vec<AssetMeta>> {
496 Err(Error::ProviderUnsupported {
497 provider: self.provider_name().to_string(),
498 operation: "get_mr_attachments".to_string(),
499 })
500 }
501
502 async fn download_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
503 Err(Error::ProviderUnsupported {
504 provider: self.provider_name().to_string(),
505 operation: "download_mr_attachment".to_string(),
506 })
507 }
508
509 async fn delete_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<()> {
510 Err(Error::ProviderUnsupported {
511 provider: self.provider_name().to_string(),
512 operation: "delete_mr_attachment".to_string(),
513 })
514 }
515}
516
517#[async_trait]
522pub trait PipelineProvider: Send + Sync {
523 fn provider_name(&self) -> &'static str;
525
526 async fn get_pipeline(&self, _input: GetPipelineInput) -> Result<PipelineInfo> {
527 Err(Error::ProviderUnsupported {
528 provider: self.provider_name().to_string(),
529 operation: "get_pipeline".to_string(),
530 })
531 }
532
533 async fn get_job_logs(&self, _job_id: &str, _options: JobLogOptions) -> Result<JobLogOutput> {
535 Err(Error::ProviderUnsupported {
536 provider: self.provider_name().to_string(),
537 operation: "get_job_logs".to_string(),
538 })
539 }
540}
541
542#[async_trait]
546pub trait Provider: IssueProvider + MergeRequestProvider + PipelineProvider {
547 async fn get_current_user(&self) -> Result<User>;
549}
550
551#[async_trait]
555pub trait MeetingNotesProvider: Send + Sync {
556 fn provider_name(&self) -> &'static str;
558
559 async fn get_meetings(&self, filter: MeetingFilter) -> Result<ProviderResult<MeetingNote>>;
561
562 async fn get_transcript(&self, meeting_id: &str) -> Result<MeetingTranscript>;
564
565 async fn search_meetings(
567 &self,
568 query: &str,
569 filter: MeetingFilter,
570 ) -> Result<ProviderResult<MeetingNote>>;
571}
572
573#[async_trait]
577pub trait KnowledgeBaseProvider: Send + Sync {
578 fn provider_name(&self) -> &'static str;
580
581 async fn get_spaces(&self) -> Result<ProviderResult<KbSpace>>;
583
584 async fn list_pages(&self, params: ListPagesParams) -> Result<ProviderResult<KbPage>>;
586
587 async fn get_page(&self, page_id: &str) -> Result<KbPageContent>;
589
590 async fn create_page(&self, params: CreatePageParams) -> Result<KbPage>;
592
593 async fn update_page(&self, params: UpdatePageParams) -> Result<KbPage>;
595
596 async fn search(&self, params: SearchKbParams) -> Result<ProviderResult<KbPage>>;
598}
599
600#[async_trait]
604pub trait MessengerProvider: Send + Sync {
605 fn provider_name(&self) -> &'static str;
607
608 async fn get_chats(&self, params: GetChatsParams) -> Result<ProviderResult<MessengerChat>>;
610
611 async fn get_messages(
613 &self,
614 params: GetMessagesParams,
615 ) -> Result<ProviderResult<MessengerMessage>>;
616
617 async fn search_messages(
619 &self,
620 params: SearchMessagesParams,
621 ) -> Result<ProviderResult<MessengerMessage>>;
622
623 async fn send_message(&self, params: SendMessageParams) -> Result<MessengerMessage>;
625}
626
627#[cfg(test)]
639mod tests {
640 use super::*;
641
642 struct DummyProvider;
646
647 #[async_trait]
648 impl IssueProvider for DummyProvider {
649 async fn get_issues(&self, _: IssueFilter) -> Result<ProviderResult<Issue>> {
650 unreachable!("the dispatcher should never call this in these tests")
651 }
652 async fn get_issue(&self, _: &str) -> Result<Issue> {
653 unreachable!()
654 }
655 async fn create_issue(&self, _: CreateIssueInput) -> Result<Issue> {
656 unreachable!()
657 }
658 async fn update_issue(&self, _: &str, _: UpdateIssueInput) -> Result<Issue> {
659 unreachable!()
660 }
661 async fn get_comments(&self, _: &str) -> Result<ProviderResult<Comment>> {
662 unreachable!()
663 }
664 async fn add_comment(&self, _: &str, _: &str) -> Result<Comment> {
665 unreachable!()
666 }
667 fn provider_name(&self) -> &'static str {
668 "dummy"
669 }
670 }
671
672 #[async_trait]
673 impl MergeRequestProvider for DummyProvider {
674 fn provider_name(&self) -> &'static str {
675 "dummy"
676 }
677 }
678
679 #[async_trait]
680 impl PipelineProvider for DummyProvider {
681 fn provider_name(&self) -> &'static str {
682 "dummy"
683 }
684 }
685
686 fn assert_unsupported<T: std::fmt::Debug>(result: Result<T>, expected_op: &str) {
689 match result {
690 Err(Error::ProviderUnsupported {
691 provider,
692 operation,
693 }) => {
694 assert_eq!(provider, "dummy");
695 assert_eq!(operation, expected_op);
696 }
697 other => panic!("expected ProviderUnsupported({expected_op}), got {other:?}"),
698 }
699 }
700
701 #[tokio::test]
706 async fn issue_provider_defaults_return_unsupported() {
707 let p = DummyProvider;
708
709 assert_unsupported(p.get_statuses().await, "get_statuses");
710 assert_unsupported(p.link_issues("a", "b", "blocks").await, "link_issues");
711 assert_unsupported(p.unlink_issues("a", "b", "blocks").await, "unlink_issues");
712 assert_unsupported(p.get_users(GetUsersOptions::default()).await, "get_users");
713 assert_unsupported(
714 p.upload_attachment("k", "f.png", b"x").await,
715 "upload_attachment",
716 );
717 assert_unsupported(p.get_issue_attachments("k").await, "get_issue_attachments");
718 assert_unsupported(p.download_attachment("k", "1").await, "download_attachment");
719 assert_unsupported(p.delete_attachment("k", "1").await, "delete_attachment");
720 assert_unsupported(p.get_issue_relations("k").await, "get_issue_relations");
721 assert_unsupported(
722 p.list_project_versions(crate::types::ListProjectVersionsParams {
723 project: "PROJ".into(),
724 ..Default::default()
725 })
726 .await,
727 "list_project_versions",
728 );
729 assert_unsupported(
730 p.upsert_project_version(crate::types::UpsertProjectVersionInput {
731 project: "PROJ".into(),
732 name: "1.0.0".into(),
733 ..Default::default()
734 })
735 .await,
736 "upsert_project_version",
737 );
738 }
739
740 #[tokio::test]
741 async fn issue_provider_set_custom_fields_is_no_op_by_default() {
742 let p = DummyProvider;
744 p.set_custom_fields("k", &[]).await.unwrap();
745 }
746
747 #[test]
748 fn issue_provider_default_asset_capabilities_is_empty() {
749 let caps = IssueProvider::asset_capabilities(&DummyProvider);
750 assert_eq!(caps, AssetCapabilities::default());
751 }
752
753 #[tokio::test]
758 async fn merge_request_provider_defaults_return_unsupported() {
759 let p = DummyProvider;
760 assert_unsupported(
761 p.get_merge_requests(MrFilter::default()).await,
762 "get_merge_requests",
763 );
764 assert_unsupported(p.get_merge_request("mr#1").await, "get_merge_request");
765 assert_unsupported(p.get_discussions("mr#1").await, "get_discussions");
766 assert_unsupported(p.get_diffs("mr#1").await, "get_diffs");
767 assert_unsupported(
768 MergeRequestProvider::add_comment(
769 &p,
770 "mr#1",
771 CreateCommentInput {
772 body: "".into(),
773 position: None,
774 discussion_id: None,
775 },
776 )
777 .await,
778 "add_merge_request_comment",
779 );
780 assert_unsupported(
781 p.create_merge_request(CreateMergeRequestInput::default())
782 .await,
783 "create_merge_request",
784 );
785 assert_unsupported(
786 p.update_merge_request("mr#1", UpdateMergeRequestInput::default())
787 .await,
788 "update_merge_request",
789 );
790 assert_unsupported(p.get_releases().await, "get_releases");
791 assert_unsupported(p.get_mr_attachments("mr#1").await, "get_mr_attachments");
792 assert_unsupported(
793 p.download_mr_attachment("mr#1", "1").await,
794 "download_mr_attachment",
795 );
796 assert_unsupported(
797 p.delete_mr_attachment("mr#1", "1").await,
798 "delete_mr_attachment",
799 );
800 }
801
802 #[tokio::test]
807 async fn pipeline_provider_defaults_return_unsupported() {
808 let p = DummyProvider;
809 assert_unsupported(
810 p.get_pipeline(GetPipelineInput::default()).await,
811 "get_pipeline",
812 );
813 assert_unsupported(
814 p.get_job_logs(
815 "1",
816 JobLogOptions {
817 mode: JobLogMode::Smart,
818 },
819 )
820 .await,
821 "get_job_logs",
822 );
823 }
824}