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, CustomFieldDescriptor, Discussion, FileDiff, ForestModifyResult,
16 GetChatsParams, GetForestOptions, GetMessagesParams, GetPipelineInput, GetStructureValuesInput,
17 GetUsersOptions, Issue, IssueFilter, IssueRelations, IssueStatus, JobLogOptions, JobLogOutput,
18 KbPage, KbPageContent, KbSpace, ListCustomFieldsParams, ListPagesParams,
19 ListProjectVersionsParams, MeetingFilter, MeetingNote, MeetingTranscript, MergeRequest,
20 MessengerChat, MessengerMessage, MoveStructureRowsInput, MrFilter, PipelineInfo,
21 ProjectVersion, ProviderResult, Release, SaveStructureViewInput, SearchKbParams,
22 SearchMessagesParams, SendMessageParams, Sprint, SprintState, Structure, StructureForest,
23 StructureGenerator, StructureValues, StructureView, SyncStructureGeneratorInput,
24 UpdateIssueInput, UpdateMergeRequestInput, UpdatePageParams, UpdateStructureAutomationInput,
25 UpsertProjectVersionInput, User,
26};
27
28#[async_trait]
32pub trait IssueProvider: Send + Sync {
33 async fn get_issues(&self, filter: IssueFilter) -> Result<ProviderResult<Issue>>;
35
36 async fn get_issue(&self, key: &str) -> Result<Issue>;
38
39 async fn create_issue(&self, input: CreateIssueInput) -> Result<Issue>;
41
42 async fn update_issue(&self, key: &str, input: UpdateIssueInput) -> Result<Issue>;
44
45 async fn get_comments(&self, issue_key: &str) -> Result<ProviderResult<Comment>>;
46
47 async fn add_comment(&self, issue_key: &str, body: &str) -> Result<Comment>;
48
49 async fn get_statuses(&self) -> Result<ProviderResult<IssueStatus>> {
52 Err(Error::ProviderUnsupported {
53 provider: self.provider_name().to_string(),
54 operation: "get_statuses".to_string(),
55 })
56 }
57
58 async fn link_issues(
60 &self,
61 _source_key: &str,
62 _target_key: &str,
63 _link_type: &str,
64 ) -> Result<()> {
65 Err(Error::ProviderUnsupported {
66 provider: self.provider_name().to_string(),
67 operation: "link_issues".to_string(),
68 })
69 }
70
71 async fn unlink_issues(
73 &self,
74 _source_key: &str,
75 _target_key: &str,
76 _link_type: &str,
77 ) -> Result<()> {
78 Err(Error::ProviderUnsupported {
79 provider: self.provider_name().to_string(),
80 operation: "unlink_issues".to_string(),
81 })
82 }
83
84 async fn get_users(&self, _options: GetUsersOptions) -> Result<ProviderResult<User>> {
86 Err(Error::ProviderUnsupported {
87 provider: self.provider_name().to_string(),
88 operation: "get_users".to_string(),
89 })
90 }
91
92 async fn upload_attachment(
95 &self,
96 _issue_key: &str,
97 _filename: &str,
98 _data: &[u8],
99 ) -> Result<String> {
100 Err(Error::ProviderUnsupported {
101 provider: self.provider_name().to_string(),
102 operation: "upload_attachment".to_string(),
103 })
104 }
105
106 async fn get_issue_attachments(&self, _issue_key: &str) -> Result<Vec<AssetMeta>> {
112 Err(Error::ProviderUnsupported {
113 provider: self.provider_name().to_string(),
114 operation: "get_issue_attachments".to_string(),
115 })
116 }
117
118 async fn download_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
124 Err(Error::ProviderUnsupported {
125 provider: self.provider_name().to_string(),
126 operation: "download_attachment".to_string(),
127 })
128 }
129
130 async fn delete_attachment(&self, _issue_key: &str, _asset_id: &str) -> Result<()> {
137 Err(Error::ProviderUnsupported {
138 provider: self.provider_name().to_string(),
139 operation: "delete_attachment".to_string(),
140 })
141 }
142
143 fn asset_capabilities(&self) -> AssetCapabilities {
148 AssetCapabilities::default()
149 }
150
151 async fn set_custom_fields(
154 &self,
155 _issue_key: &str,
156 _fields: &[serde_json::Value],
157 ) -> Result<()> {
158 Ok(()) }
160
161 async fn get_issue_relations(&self, _issue_key: &str) -> Result<IssueRelations> {
163 Err(Error::ProviderUnsupported {
164 provider: self.provider_name().to_string(),
165 operation: "get_issue_relations".to_string(),
166 })
167 }
168
169 async fn get_structures(&self) -> Result<ProviderResult<Structure>> {
174 Err(Error::ProviderUnsupported {
175 provider: self.provider_name().to_string(),
176 operation: "get_structures".to_string(),
177 })
178 }
179
180 async fn get_structure_forest(
182 &self,
183 _structure_id: u64,
184 _options: GetForestOptions,
185 ) -> Result<StructureForest> {
186 Err(Error::ProviderUnsupported {
187 provider: self.provider_name().to_string(),
188 operation: "get_structure_forest".to_string(),
189 })
190 }
191
192 async fn add_structure_rows(
194 &self,
195 _structure_id: u64,
196 _input: AddStructureRowsInput,
197 ) -> Result<ForestModifyResult> {
198 Err(Error::ProviderUnsupported {
199 provider: self.provider_name().to_string(),
200 operation: "add_structure_rows".to_string(),
201 })
202 }
203
204 async fn move_structure_rows(
206 &self,
207 _structure_id: u64,
208 _input: MoveStructureRowsInput,
209 ) -> Result<ForestModifyResult> {
210 Err(Error::ProviderUnsupported {
211 provider: self.provider_name().to_string(),
212 operation: "move_structure_rows".to_string(),
213 })
214 }
215
216 async fn remove_structure_row(&self, _structure_id: u64, _row_id: u64) -> Result<()> {
218 Err(Error::ProviderUnsupported {
219 provider: self.provider_name().to_string(),
220 operation: "remove_structure_row".to_string(),
221 })
222 }
223
224 async fn get_structure_values(
226 &self,
227 _input: GetStructureValuesInput,
228 ) -> Result<StructureValues> {
229 Err(Error::ProviderUnsupported {
230 provider: self.provider_name().to_string(),
231 operation: "get_structure_values".to_string(),
232 })
233 }
234
235 async fn get_structure_views(
237 &self,
238 _structure_id: u64,
239 _view_id: Option<u64>,
240 ) -> Result<Vec<StructureView>> {
241 Err(Error::ProviderUnsupported {
242 provider: self.provider_name().to_string(),
243 operation: "get_structure_views".to_string(),
244 })
245 }
246
247 async fn save_structure_view(&self, _input: SaveStructureViewInput) -> Result<StructureView> {
249 Err(Error::ProviderUnsupported {
250 provider: self.provider_name().to_string(),
251 operation: "save_structure_view".to_string(),
252 })
253 }
254
255 async fn create_structure(&self, _input: CreateStructureInput) -> Result<Structure> {
257 Err(Error::ProviderUnsupported {
258 provider: self.provider_name().to_string(),
259 operation: "create_structure".to_string(),
260 })
261 }
262
263 async fn get_structure_generators(
267 &self,
268 _structure_id: u64,
269 ) -> Result<ProviderResult<StructureGenerator>> {
270 Err(Error::ProviderUnsupported {
271 provider: self.provider_name().to_string(),
272 operation: "get_structure_generators".to_string(),
273 })
274 }
275
276 async fn add_structure_generator(
278 &self,
279 _input: AddStructureGeneratorInput,
280 ) -> Result<StructureGenerator> {
281 Err(Error::ProviderUnsupported {
282 provider: self.provider_name().to_string(),
283 operation: "add_structure_generator".to_string(),
284 })
285 }
286
287 async fn sync_structure_generator(&self, _input: SyncStructureGeneratorInput) -> Result<()> {
289 Err(Error::ProviderUnsupported {
290 provider: self.provider_name().to_string(),
291 operation: "sync_structure_generator".to_string(),
292 })
293 }
294
295 async fn delete_structure(&self, _structure_id: u64) -> Result<()> {
299 Err(Error::ProviderUnsupported {
300 provider: self.provider_name().to_string(),
301 operation: "delete_structure".to_string(),
302 })
303 }
304
305 async fn update_structure_automation(
307 &self,
308 _input: UpdateStructureAutomationInput,
309 ) -> Result<()> {
310 Err(Error::ProviderUnsupported {
311 provider: self.provider_name().to_string(),
312 operation: "update_structure_automation".to_string(),
313 })
314 }
315
316 async fn trigger_structure_automation(&self, _structure_id: u64) -> Result<()> {
318 Err(Error::ProviderUnsupported {
319 provider: self.provider_name().to_string(),
320 operation: "trigger_structure_automation".to_string(),
321 })
322 }
323
324 async fn list_project_versions(
334 &self,
335 _params: ListProjectVersionsParams,
336 ) -> Result<ProviderResult<ProjectVersion>> {
337 Err(Error::ProviderUnsupported {
338 provider: self.provider_name().to_string(),
339 operation: "list_project_versions".to_string(),
340 })
341 }
342
343 async fn upsert_project_version(
347 &self,
348 _input: UpsertProjectVersionInput,
349 ) -> Result<ProjectVersion> {
350 Err(Error::ProviderUnsupported {
351 provider: self.provider_name().to_string(),
352 operation: "upsert_project_version".to_string(),
353 })
354 }
355
356 async fn get_board_sprints(
360 &self,
361 _board_id: u64,
362 _state: SprintState,
363 ) -> Result<ProviderResult<Sprint>> {
364 Err(Error::ProviderUnsupported {
365 provider: self.provider_name().to_string(),
366 operation: "get_board_sprints".to_string(),
367 })
368 }
369
370 async fn assign_to_sprint(&self, _input: AssignToSprintInput) -> Result<()> {
372 Err(Error::ProviderUnsupported {
373 provider: self.provider_name().to_string(),
374 operation: "assign_to_sprint".to_string(),
375 })
376 }
377
378 async fn list_custom_fields(
384 &self,
385 _params: ListCustomFieldsParams,
386 ) -> Result<ProviderResult<CustomFieldDescriptor>> {
387 Err(Error::ProviderUnsupported {
388 provider: self.provider_name().to_string(),
389 operation: "list_custom_fields".to_string(),
390 })
391 }
392
393 fn provider_name(&self) -> &'static str;
395}
396
397#[async_trait]
410pub trait UserProvider: Send + Sync {
411 fn provider_name(&self) -> &'static str;
413
414 async fn get_user_profile(&self, _user_id: &str) -> Result<User> {
417 Err(Error::ProviderUnsupported {
418 provider: self.provider_name().to_string(),
419 operation: "get_user_profile".to_string(),
420 })
421 }
422
423 async fn lookup_user_by_email(&self, _email: &str) -> Result<Option<User>> {
427 Err(Error::ProviderUnsupported {
428 provider: self.provider_name().to_string(),
429 operation: "lookup_user_by_email".to_string(),
430 })
431 }
432}
433
434#[async_trait]
440pub trait MergeRequestProvider: Send + Sync {
441 fn provider_name(&self) -> &'static str;
443
444 async fn get_merge_requests(&self, _filter: MrFilter) -> Result<ProviderResult<MergeRequest>> {
446 Err(Error::ProviderUnsupported {
447 provider: self.provider_name().to_string(),
448 operation: "get_merge_requests".to_string(),
449 })
450 }
451
452 async fn get_merge_request(&self, _key: &str) -> Result<MergeRequest> {
454 Err(Error::ProviderUnsupported {
455 provider: self.provider_name().to_string(),
456 operation: "get_merge_request".to_string(),
457 })
458 }
459
460 async fn get_discussions(&self, _mr_key: &str) -> Result<ProviderResult<Discussion>> {
462 Err(Error::ProviderUnsupported {
463 provider: self.provider_name().to_string(),
464 operation: "get_discussions".to_string(),
465 })
466 }
467
468 async fn get_diffs(&self, _mr_key: &str) -> Result<ProviderResult<FileDiff>> {
469 Err(Error::ProviderUnsupported {
470 provider: self.provider_name().to_string(),
471 operation: "get_diffs".to_string(),
472 })
473 }
474
475 async fn add_comment(&self, _mr_key: &str, _input: CreateCommentInput) -> Result<Comment> {
476 Err(Error::ProviderUnsupported {
477 provider: self.provider_name().to_string(),
478 operation: "add_merge_request_comment".to_string(),
479 })
480 }
481
482 async fn create_merge_request(&self, _input: CreateMergeRequestInput) -> Result<MergeRequest> {
484 Err(Error::ProviderUnsupported {
485 provider: self.provider_name().to_string(),
486 operation: "create_merge_request".to_string(),
487 })
488 }
489
490 async fn update_merge_request(
492 &self,
493 _key: &str,
494 _input: UpdateMergeRequestInput,
495 ) -> Result<MergeRequest> {
496 Err(Error::ProviderUnsupported {
497 provider: self.provider_name().to_string(),
498 operation: "update_merge_request".to_string(),
499 })
500 }
501
502 async fn get_releases(&self) -> Result<ProviderResult<Release>> {
504 Err(Error::ProviderUnsupported {
505 provider: self.provider_name().to_string(),
506 operation: "get_releases".to_string(),
507 })
508 }
509
510 async fn get_mr_attachments(&self, _mr_key: &str) -> Result<Vec<AssetMeta>> {
512 Err(Error::ProviderUnsupported {
513 provider: self.provider_name().to_string(),
514 operation: "get_mr_attachments".to_string(),
515 })
516 }
517
518 async fn download_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<Vec<u8>> {
519 Err(Error::ProviderUnsupported {
520 provider: self.provider_name().to_string(),
521 operation: "download_mr_attachment".to_string(),
522 })
523 }
524
525 async fn delete_mr_attachment(&self, _mr_key: &str, _asset_id: &str) -> Result<()> {
526 Err(Error::ProviderUnsupported {
527 provider: self.provider_name().to_string(),
528 operation: "delete_mr_attachment".to_string(),
529 })
530 }
531}
532
533#[async_trait]
538pub trait PipelineProvider: Send + Sync {
539 fn provider_name(&self) -> &'static str;
541
542 async fn get_pipeline(&self, _input: GetPipelineInput) -> Result<PipelineInfo> {
543 Err(Error::ProviderUnsupported {
544 provider: self.provider_name().to_string(),
545 operation: "get_pipeline".to_string(),
546 })
547 }
548
549 async fn get_job_logs(&self, _job_id: &str, _options: JobLogOptions) -> Result<JobLogOutput> {
551 Err(Error::ProviderUnsupported {
552 provider: self.provider_name().to_string(),
553 operation: "get_job_logs".to_string(),
554 })
555 }
556}
557
558#[async_trait]
562pub trait Provider: IssueProvider + MergeRequestProvider + PipelineProvider {
563 async fn get_current_user(&self) -> Result<User>;
565}
566
567#[async_trait]
571pub trait MeetingNotesProvider: Send + Sync {
572 fn provider_name(&self) -> &'static str;
574
575 async fn get_meetings(&self, filter: MeetingFilter) -> Result<ProviderResult<MeetingNote>>;
577
578 async fn get_transcript(&self, meeting_id: &str) -> Result<MeetingTranscript>;
580
581 async fn search_meetings(
583 &self,
584 query: &str,
585 filter: MeetingFilter,
586 ) -> Result<ProviderResult<MeetingNote>>;
587}
588
589#[async_trait]
593pub trait KnowledgeBaseProvider: Send + Sync {
594 fn provider_name(&self) -> &'static str;
596
597 async fn get_spaces(&self) -> Result<ProviderResult<KbSpace>>;
599
600 async fn list_pages(&self, params: ListPagesParams) -> Result<ProviderResult<KbPage>>;
602
603 async fn get_page(&self, page_id: &str) -> Result<KbPageContent>;
605
606 async fn create_page(&self, params: CreatePageParams) -> Result<KbPage>;
608
609 async fn update_page(&self, params: UpdatePageParams) -> Result<KbPage>;
611
612 async fn search(&self, params: SearchKbParams) -> Result<ProviderResult<KbPage>>;
614}
615
616#[async_trait]
620pub trait MessengerProvider: Send + Sync {
621 fn provider_name(&self) -> &'static str;
623
624 async fn get_chats(&self, params: GetChatsParams) -> Result<ProviderResult<MessengerChat>>;
626
627 async fn get_messages(
629 &self,
630 params: GetMessagesParams,
631 ) -> Result<ProviderResult<MessengerMessage>>;
632
633 async fn search_messages(
635 &self,
636 params: SearchMessagesParams,
637 ) -> Result<ProviderResult<MessengerMessage>>;
638
639 async fn send_message(&self, params: SendMessageParams) -> Result<MessengerMessage>;
641}
642
643#[cfg(test)]
655mod tests {
656 use super::*;
657
658 struct DummyProvider;
662
663 #[async_trait]
664 impl IssueProvider for DummyProvider {
665 async fn get_issues(&self, _: IssueFilter) -> Result<ProviderResult<Issue>> {
666 unreachable!("the dispatcher should never call this in these tests")
667 }
668 async fn get_issue(&self, _: &str) -> Result<Issue> {
669 unreachable!()
670 }
671 async fn create_issue(&self, _: CreateIssueInput) -> Result<Issue> {
672 unreachable!()
673 }
674 async fn update_issue(&self, _: &str, _: UpdateIssueInput) -> Result<Issue> {
675 unreachable!()
676 }
677 async fn get_comments(&self, _: &str) -> Result<ProviderResult<Comment>> {
678 unreachable!()
679 }
680 async fn add_comment(&self, _: &str, _: &str) -> Result<Comment> {
681 unreachable!()
682 }
683 fn provider_name(&self) -> &'static str {
684 "dummy"
685 }
686 }
687
688 #[async_trait]
689 impl MergeRequestProvider for DummyProvider {
690 fn provider_name(&self) -> &'static str {
691 "dummy"
692 }
693 }
694
695 #[async_trait]
696 impl PipelineProvider for DummyProvider {
697 fn provider_name(&self) -> &'static str {
698 "dummy"
699 }
700 }
701
702 fn assert_unsupported<T: std::fmt::Debug>(result: Result<T>, expected_op: &str) {
705 match result {
706 Err(Error::ProviderUnsupported {
707 provider,
708 operation,
709 }) => {
710 assert_eq!(provider, "dummy");
711 assert_eq!(operation, expected_op);
712 }
713 other => panic!("expected ProviderUnsupported({expected_op}), got {other:?}"),
714 }
715 }
716
717 #[tokio::test]
722 async fn issue_provider_defaults_return_unsupported() {
723 let p = DummyProvider;
724
725 assert_unsupported(p.get_statuses().await, "get_statuses");
726 assert_unsupported(p.link_issues("a", "b", "blocks").await, "link_issues");
727 assert_unsupported(p.unlink_issues("a", "b", "blocks").await, "unlink_issues");
728 assert_unsupported(p.get_users(GetUsersOptions::default()).await, "get_users");
729 assert_unsupported(
730 p.upload_attachment("k", "f.png", b"x").await,
731 "upload_attachment",
732 );
733 assert_unsupported(p.get_issue_attachments("k").await, "get_issue_attachments");
734 assert_unsupported(p.download_attachment("k", "1").await, "download_attachment");
735 assert_unsupported(p.delete_attachment("k", "1").await, "delete_attachment");
736 assert_unsupported(p.get_issue_relations("k").await, "get_issue_relations");
737 assert_unsupported(
738 p.list_project_versions(crate::types::ListProjectVersionsParams {
739 project: "PROJ".into(),
740 ..Default::default()
741 })
742 .await,
743 "list_project_versions",
744 );
745 assert_unsupported(
746 p.upsert_project_version(crate::types::UpsertProjectVersionInput {
747 project: "PROJ".into(),
748 name: "1.0.0".into(),
749 ..Default::default()
750 })
751 .await,
752 "upsert_project_version",
753 );
754 }
755
756 #[tokio::test]
757 async fn issue_provider_set_custom_fields_is_no_op_by_default() {
758 let p = DummyProvider;
760 p.set_custom_fields("k", &[]).await.unwrap();
761 }
762
763 #[test]
764 fn issue_provider_default_asset_capabilities_is_empty() {
765 let caps = IssueProvider::asset_capabilities(&DummyProvider);
766 assert_eq!(caps, AssetCapabilities::default());
767 }
768
769 #[tokio::test]
774 async fn merge_request_provider_defaults_return_unsupported() {
775 let p = DummyProvider;
776 assert_unsupported(
777 p.get_merge_requests(MrFilter::default()).await,
778 "get_merge_requests",
779 );
780 assert_unsupported(p.get_merge_request("mr#1").await, "get_merge_request");
781 assert_unsupported(p.get_discussions("mr#1").await, "get_discussions");
782 assert_unsupported(p.get_diffs("mr#1").await, "get_diffs");
783 assert_unsupported(
784 MergeRequestProvider::add_comment(
785 &p,
786 "mr#1",
787 CreateCommentInput {
788 body: "".into(),
789 position: None,
790 discussion_id: None,
791 },
792 )
793 .await,
794 "add_merge_request_comment",
795 );
796 assert_unsupported(
797 p.create_merge_request(CreateMergeRequestInput::default())
798 .await,
799 "create_merge_request",
800 );
801 assert_unsupported(
802 p.update_merge_request("mr#1", UpdateMergeRequestInput::default())
803 .await,
804 "update_merge_request",
805 );
806 assert_unsupported(p.get_releases().await, "get_releases");
807 assert_unsupported(p.get_mr_attachments("mr#1").await, "get_mr_attachments");
808 assert_unsupported(
809 p.download_mr_attachment("mr#1", "1").await,
810 "download_mr_attachment",
811 );
812 assert_unsupported(
813 p.delete_mr_attachment("mr#1", "1").await,
814 "delete_mr_attachment",
815 );
816 }
817
818 #[tokio::test]
823 async fn pipeline_provider_defaults_return_unsupported() {
824 let p = DummyProvider;
825 assert_unsupported(
826 p.get_pipeline(GetPipelineInput::default()).await,
827 "get_pipeline",
828 );
829 assert_unsupported(
830 p.get_job_logs(
831 "1",
832 JobLogOptions {
833 mode: JobLogMode::Smart,
834 },
835 )
836 .await,
837 "get_job_logs",
838 );
839 }
840}