tod 0.16.0

An unofficial Todoist command-line client
use crate::config::{SortKey, SortRule};
use serde::{Deserialize, Serialize};
use std::sync::Once;

static SORT_VALUE_WARNING: Once = Once::new();

#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct LegacySortValue {
    pub priority_none: Option<u8>,
    pub priority_low: Option<u8>,
    pub priority_medium: Option<u8>,
    pub priority_high: Option<u8>,
    pub no_due_date: Option<u8>,
    pub not_recurring: Option<u8>,
    pub today: Option<u8>,
    pub overdue: Option<u8>,
    pub now: Option<u8>,
    pub deadline_value: Option<u8>,
    pub deadline_days: Option<u8>,
}

pub(crate) fn detect_and_migrate_sort_value(
    sort_value: Option<&LegacySortValue>,
) -> Option<Vec<SortRule>> {
    let sort_value = sort_value?;
    SORT_VALUE_WARNING.call_once(|| {
        eprintln!(
            "Legacy sort_value config detected; migrating to sort_order. \
             Please update your config because sort_value will be removed in a future version."
        );
    });

    let priority = [
        sort_value.priority_none,
        sort_value.priority_low,
        sort_value.priority_medium,
        sort_value.priority_high,
    ]
    .into_iter()
    .flatten()
    .max()
    .unwrap_or_default();

    let mut weighted_keys = vec![
        (SortKey::Priority, priority),
        (SortKey::Overdue, sort_value.overdue.unwrap_or_default()),
        (SortKey::Today, sort_value.today.unwrap_or_default()),
        (SortKey::Now, sort_value.now.unwrap_or_default()),
        (
            SortKey::NoDueDate,
            sort_value.no_due_date.unwrap_or_default(),
        ),
        (
            SortKey::NotRecurring,
            sort_value.not_recurring.unwrap_or_default(),
        ),
        (
            SortKey::Deadline,
            sort_value.deadline_value.unwrap_or_default(),
        ),
    ];

    weighted_keys.sort_by(|(left_key, left_weight), (right_key, right_weight)| {
        right_weight.cmp(left_weight).then_with(|| {
            sort_key_default_index(*left_key).cmp(&sort_key_default_index(*right_key))
        })
    });

    let mut keys: Vec<SortKey> = weighted_keys.into_iter().map(|(key, _)| key).collect();
    for default_key in SortKey::default_order() {
        if !keys.contains(&default_key) {
            keys.push(default_key);
        }
    }

    Some(
        keys.into_iter()
            .map(SortRule::with_default_direction)
            .collect(),
    )
}

fn sort_key_default_index(key: SortKey) -> usize {
    SortKey::default_order()
        .iter()
        .position(|default_key| *default_key == key)
        .unwrap_or(usize::MAX)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detect_and_migrate_sort_value_returns_none_for_missing_input() {
        assert_eq!(detect_and_migrate_sort_value(None), None);
    }

    #[test]
    fn detect_and_migrate_sort_value_prioritizes_highest_weighted_key() {
        let legacy = LegacySortValue {
            priority_none: Some(0),
            priority_low: Some(0),
            priority_medium: Some(0),
            priority_high: Some(0),
            no_due_date: Some(0),
            not_recurring: Some(0),
            today: Some(0),
            overdue: Some(9),
            now: Some(0),
            deadline_value: Some(1),
            deadline_days: Some(0),
        };

        let migrated = detect_and_migrate_sort_value(Some(&legacy))
            .expect("legacy sort config should migrate");
        let first = migrated
            .first()
            .expect("migrated sort order should not be empty");
        assert_eq!(first.key, SortKey::Overdue);
    }
}