Stub rescheduling algo
This commit is contained in:
parent
b3e0270e38
commit
adc6b7866a
4 changed files with 186 additions and 12 deletions
121
src/collect.rs
121
src/collect.rs
|
|
@ -1,6 +1,7 @@
|
|||
use crate::task;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use forgejo_api::structs::Issue;
|
||||
|
||||
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
|
||||
let issues = list_all_issues(ctx).await?;
|
||||
|
|
@ -20,18 +21,88 @@ pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Tas
|
|||
Ok(tasks)
|
||||
}
|
||||
|
||||
fn task_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result<task::Task> {
|
||||
fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
|
||||
Ok(task::Task {
|
||||
issue_number: issue
|
||||
.number
|
||||
.context("Missing issue number")?
|
||||
.try_into()
|
||||
.context("Failed converting issue number")?,
|
||||
title: issue.title.as_ref().context("Missing issue title")?.clone(),
|
||||
issue_number: issue.get_number()?,
|
||||
title: issue.get_title()?,
|
||||
state: task_state_from_issue(issue)?,
|
||||
recurring: task_recurring_from_issue_labels(issue)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<forgejo_api::structs::Issue>> {
|
||||
fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> {
|
||||
match issue.get_state()? {
|
||||
forgejo_api::structs::StateType::Open => Ok(task::State::Open {
|
||||
due: issue.due_date,
|
||||
}),
|
||||
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
|
||||
date: issue
|
||||
.closed_at
|
||||
.context("Closed issue without a closed_at date")?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn task_recurring_from_issue_labels(issue: &Issue) -> anyhow::Result<Option<task::Recurring>> {
|
||||
let labels = issue.get_label_names()?;
|
||||
let Some(recurring_label) = get_recurring_label(&labels)? else {
|
||||
return Ok(None)
|
||||
};
|
||||
|
||||
let interval = match recurring_label {
|
||||
// Months
|
||||
"Every Month" => task::RecurringInterval::Months(1),
|
||||
"Every 2 Months" => task::RecurringInterval::Months(2),
|
||||
"Every 3 Months" => task::RecurringInterval::Months(3),
|
||||
"Every 4 Months" => task::RecurringInterval::Months(4),
|
||||
"Every 5 Months" => task::RecurringInterval::Months(5),
|
||||
"Every 6 Months" => task::RecurringInterval::Months(6),
|
||||
"Every 7 Months" => task::RecurringInterval::Months(7),
|
||||
"Every 8 Months" => task::RecurringInterval::Months(8),
|
||||
"Every 9 Months" => task::RecurringInterval::Months(9),
|
||||
"Every 10 Months" => task::RecurringInterval::Months(10),
|
||||
"Every 11 Months" => task::RecurringInterval::Months(11),
|
||||
"Every Year" => task::RecurringInterval::Months(12),
|
||||
// Weeks
|
||||
"Every Week" => task::RecurringInterval::Weeks(1),
|
||||
"Every 2 Weeks" => task::RecurringInterval::Weeks(2),
|
||||
"Every 3 Weeks" => task::RecurringInterval::Weeks(3),
|
||||
"Every 4 Weeks" => task::RecurringInterval::Weeks(4),
|
||||
"Every 5 Weeks" => task::RecurringInterval::Weeks(5),
|
||||
"Every 6 Weeks" => task::RecurringInterval::Weeks(6),
|
||||
"Every 7 Weeks" => task::RecurringInterval::Weeks(7),
|
||||
"Every 8 Weeks" => task::RecurringInterval::Weeks(8),
|
||||
"Every 9 Weeks" => task::RecurringInterval::Weeks(9),
|
||||
"Every 10 Weeks" => task::RecurringInterval::Weeks(10),
|
||||
// Days
|
||||
"Every Day" => task::RecurringInterval::Days(1),
|
||||
"Every 2 Days" => task::RecurringInterval::Days(2),
|
||||
"Every 3 Days" => task::RecurringInterval::Days(3),
|
||||
"Every 4 Days" => task::RecurringInterval::Days(4),
|
||||
"Every 5 Days" => task::RecurringInterval::Days(5),
|
||||
"Every 6 Days" => task::RecurringInterval::Days(6),
|
||||
// Fallback
|
||||
s => anyhow::bail!("Unknown recurring interval: {s:?}"),
|
||||
};
|
||||
|
||||
Ok(Some(task::Recurring { interval }))
|
||||
}
|
||||
|
||||
fn get_recurring_label(labels: &[String]) -> anyhow::Result<Option<&str>> {
|
||||
let mut recurring_labels_iter = labels
|
||||
.iter()
|
||||
.filter_map(|label| label.strip_prefix("Recurring/"));
|
||||
let Some(recurring_label) = recurring_labels_iter.next() else {
|
||||
// No recurring label means this is not a recurring task
|
||||
return Ok(None);
|
||||
};
|
||||
if recurring_labels_iter.next().is_some() {
|
||||
anyhow::bail!("More than one Recurring/ label found on issue");
|
||||
}
|
||||
Ok(Some(recurring_label))
|
||||
}
|
||||
|
||||
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<Issue>> {
|
||||
let issues = ctx
|
||||
.forgejo
|
||||
.issue_list_issues(
|
||||
|
|
@ -51,3 +122,37 @@ async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<forgejo_api
|
|||
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
trait IssueExt {
|
||||
fn get_number(&self) -> anyhow::Result<u64>;
|
||||
fn get_title(&self) -> anyhow::Result<String>;
|
||||
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType>;
|
||||
fn get_label_names(&self) -> anyhow::Result<Vec<String>>;
|
||||
}
|
||||
|
||||
impl IssueExt for Issue {
|
||||
fn get_number(&self) -> anyhow::Result<u64> {
|
||||
Ok(self
|
||||
.number
|
||||
.context("Missing issue number")?
|
||||
.try_into()
|
||||
.context("Failed converting issue number to u64")?)
|
||||
}
|
||||
|
||||
fn get_title(&self) -> anyhow::Result<String> {
|
||||
Ok(self.title.as_ref().context("Missing issue title")?.clone())
|
||||
}
|
||||
|
||||
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType> {
|
||||
Ok(self.state.context("Issue has no state")?)
|
||||
}
|
||||
|
||||
fn get_label_names(&self) -> anyhow::Result<Vec<std::string::String>> {
|
||||
let labels = self.labels.as_ref().context("Issue without labels list")?;
|
||||
let label_names = labels
|
||||
.into_iter()
|
||||
.map(|label| label.name.as_ref().context("Label without name").cloned())
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(label_names)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async fn run() -> anyhow::Result<()> {
|
|||
|
||||
let tasks = collect::collect_tasks(&ctx).await?;
|
||||
|
||||
scheduler::schedule_recurring_tasks(&ctx, &tasks).await?;
|
||||
scheduler::reschedule_recurring_tasks(&ctx, &tasks).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,40 @@
|
|||
use crate::task;
|
||||
|
||||
pub async fn schedule_recurring_tasks(
|
||||
pub async fn reschedule_recurring_tasks(
|
||||
ctx: &crate::Context,
|
||||
tasks: &[task::Task],
|
||||
) -> anyhow::Result<()> {
|
||||
dbg!(tasks);
|
||||
let mut rescheduled = 0;
|
||||
for task in tasks {
|
||||
let task::State::Completed {
|
||||
date: completed_date,
|
||||
} = &task.state
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(recurring) = &task.recurring else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let due_date = next_due_date(*completed_date, recurring.interval);
|
||||
|
||||
log::info!("Rescheduling {task} for {due_date}...");
|
||||
reopen_issue_with_due_date(ctx, task, due_date).await?;
|
||||
rescheduled += 1;
|
||||
}
|
||||
|
||||
if rescheduled == 0 {
|
||||
log::info!("No tasks need rescheduling.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_due_date(completed_date: time::OffsetDateTime, interval: task::RecurringInterval) -> time::OffsetDateTime {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn reopen_issue_with_due_date(ctx: &crate::Context, task: &task::Task, due_date: time::OffsetDateTime) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
|||
40
src/task.rs
40
src/task.rs
|
|
@ -1,5 +1,43 @@
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Task {
|
||||
/// Issue Number for referencing the task
|
||||
pub issue_number: u64,
|
||||
|
||||
/// Human-readable summary of the task
|
||||
pub title: String,
|
||||
|
||||
/// Whether the task is open or has been completed
|
||||
pub state: State,
|
||||
|
||||
/// Whether the task is a recurring one and metadata for rescheduling
|
||||
pub recurring: Option<Recurring>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
/// The task is open and pending completion.
|
||||
///
|
||||
/// An optional due date may be present.
|
||||
Open { due: Option<time::OffsetDateTime> },
|
||||
|
||||
/// The task has been completed at the specified time.
|
||||
Completed { date: time::OffsetDateTime },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Recurring {
|
||||
pub interval: RecurringInterval,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RecurringInterval {
|
||||
Months(u32),
|
||||
Weeks(u32),
|
||||
Days(u32),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Task {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "#{} — \"{}\"", self.issue_number, self.title)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue