Skip to main content

tatuin_providers/
github_issues.rs

1// SPDX-License-Identifier: MIT
2
3use 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}