tatuin_providers/
github_issues.rs1use crate::config::Config;
4
5use super::github::{client::Client, structs};
6use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
7use std::any::Any;
8use tatuin_core::{
9 RichString, StringError, filter,
10 project::Project as ProjectTrait,
11 provider::{Capabilities, ProjectProviderTrait, ProviderTrait, TaskProviderTrait},
12 task::{DateTimeUtc, PatchPolicy, State, Task as TaskTrait, due_group},
13 task_patch::{PatchError, TaskPatch},
14};
15
16use async_trait::async_trait;
17
18pub const PROVIDER_NAME: &str = "GitHub Issues";
19
20#[derive(Clone)]
21pub struct Task {
22 issue: structs::Issue,
23 provider: String,
24}
25
26fn str_to_date(s: &str) -> Option<DateTimeUtc> {
27 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
28 let dt = d.and_hms_opt(0, 0, 0)?;
29 return Some(DateTimeUtc::from_naive_utc_and_offset(dt, Utc));
30 }
31
32 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
33 return Some(DateTimeUtc::from_naive_utc_and_offset(dt, Utc));
34 }
35
36 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
37 return Some(DateTimeUtc::from(dt));
38 }
39
40 None
41}
42
43impl TaskTrait for Task {
44 fn id(&self) -> String {
45 self.issue.id.to_string()
46 }
47
48 fn name(&self) -> RichString {
49 RichString::new(&self.issue.title)
50 }
51
52 fn created_at(&self) -> Option<DateTimeUtc> {
53 str_to_date(self.issue.created_at.as_str())
54 }
55
56 fn due(&self) -> Option<DateTimeUtc> {
57 if let Some(m) = &self.issue.milestone
58 && let Some(due) = &m.due_on
59 {
60 return str_to_date(due.as_str());
61 }
62
63 None
64 }
65
66 fn place(&self) -> String {
67 self.issue.html_url.to_string()
68 }
69
70 fn state(&self) -> State {
71 match self.issue.state.as_str() {
72 "open" => State::Uncompleted,
73 "closed" => State::Completed,
74 _ => panic!("Unknown state {}", self.issue.state),
75 }
76 }
77
78 fn provider(&self) -> String {
79 self.provider.to_string()
80 }
81
82 fn project(&self) -> Option<Box<dyn ProjectTrait>> {
83 None
84 }
85
86 fn as_any(&self) -> &dyn Any {
87 self
88 }
89
90 fn url(&self) -> String {
91 self.issue.html_url.to_string()
92 }
93
94 fn clone_boxed(&self) -> Box<dyn TaskTrait> {
95 Box::new(self.clone())
96 }
97 fn const_patch_policy(&self) -> PatchPolicy {
98 PatchPolicy {
99 is_editable: false,
100 is_removable: false,
101 available_states: Vec::new(),
102 available_priorities: Vec::new(),
103 available_due_items: Vec::new(),
104 available_scheduled_items: Vec::new(),
105 }
106 }
107}
108
109pub struct Provider {
110 cfg: Config,
111 repo: String,
112 client: Client,
113 tasks: Vec<Task>,
114 last_filter: Option<filter::Filter>,
115}
116
117impl Provider {
118 pub fn new(cfg: Config, api_key: &str, repo: &str) -> Self {
119 Self {
120 cfg,
121 repo: repo.to_string(),
122 client: Client::new(api_key),
123 tasks: Vec::new(),
124 last_filter: None,
125 }
126 }
127}
128
129impl std::fmt::Debug for Provider {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 write!(f, "Provider name={}", ProviderTrait::name(self))
132 }
133}
134
135#[async_trait]
136impl ProjectProviderTrait for Provider {
137 async fn list(&mut self) -> Result<Vec<Box<dyn ProjectTrait>>, StringError> {
138 Ok(Vec::new())
139 }
140}
141
142#[async_trait]
143impl TaskProviderTrait for Provider {
144 async fn list(
145 &mut self,
146 _project: Option<Box<dyn ProjectTrait>>,
147 f: &filter::Filter,
148 ) -> Result<Vec<Box<dyn TaskTrait>>, StringError> {
149 let mut should_clear = false;
150 if let Some(last_filter) = self.last_filter.as_mut() {
151 should_clear = last_filter != f;
152 }
153
154 if should_clear {
155 self.tasks.clear();
156 }
157
158 if self.tasks.is_empty() {
159 for t in self.client.issues(&self.repo, &f.states).await? {
160 self.tasks.push(Task {
161 issue: t,
162 provider: self.name(),
163 })
164 }
165 }
166
167 let mut result: Vec<Box<dyn TaskTrait>> = Vec::new();
168
169 for t in &self.tasks {
170 if f.due.contains(&due_group(&t.due())) {
171 result.push(Box::new(t.clone()));
172 }
173 }
174
175 self.last_filter = Some(f.clone());
176
177 Ok(result)
178 }
179
180 async fn create(&mut self, _project_id: &str, _tp: &TaskPatch) -> Result<(), StringError> {
181 Err(StringError::new("Task creation is not supported"))
182 }
183
184 async fn update(&mut self, _patches: &[TaskPatch]) -> Vec<PatchError> {
185 Vec::new()
186 }
187}
188
189#[async_trait]
190impl ProviderTrait for Provider {
191 fn name(&self) -> String {
192 self.cfg.name()
193 }
194
195 fn type_name(&self) -> String {
196 PROVIDER_NAME.to_string()
197 }
198
199 async fn reload(&mut self) {
200 self.tasks.clear();
201 }
202
203 fn capabilities(&self) -> Capabilities {
204 Capabilities { create_task: false }
205 }
206}