tudor-sql 0.1.0

Does sql stuff to todo.txt files
// TODO rm me
#![allow(warnings)]

use std::collections::BTreeSet;

use log::*;
use typed_builder::TypedBuilder;

use crate::sql::SelectFields::Star;
use crate::sql::SelectFrom::Stdin;
use crate::todo::Todo;

#[derive(Eq, Ord, PartialOrd, PartialEq, Debug)]
enum Value {
    Null,
    True,
    False,
    // Now,
    Date(time::Date),
    String_(String),
    // Array(Vec<Value>),
    Set(BTreeSet<Value>),
    // Array(Vec<String>) ??
}

impl Value {
    pub fn is_null(&self) -> bool {
        match self {
            Value::Null => true,
            _ => false,
        }
    }
}

// TODO: Function is not a field
//       there's something else that is either a field or a function with that something else
#[derive(Eq, PartialEq, Hash, Debug)]
enum Field {
    FullText,
    IsCompleted,
    // no pri, completion, creation/completion date, tags: (h:, dep:) ???
    // TaskText, TODO:
    // Id,
    // IsBlockingFor,
    // IsBlocked,
    // BlockedBy?
    CreationDate,
    CompletionDate,
    Priority,
    CanonicalContext,
    Contexts,
    CanonicalProject,
    Projects,
    DueDate,
    ThresholdDate,
    IsHidden,
    // Function(Function, Box<Field>),
}

impl Field {
    fn map_or_null<T, F: FnOnce(T) -> Value>(o: Option<T>, f: F) -> Value {
        match o {
            Some(t) => f(t),
            None => Value::Null,
        }
    }
    fn bool_to_value(b: bool) -> Value {
        if b {
            Value::True
        } else {
            Value::False
        }
    }
    fn get_value(&self, t: &Todo) -> Value {
        let val = match self {
            Field::FullText => unimplemented!(), // TODO: implement display for Todo
            Field::IsCompleted => Field::bool_to_value(t.is_completed),
            Field::CreationDate => Field::map_or_null(t.creation_date, |d| Value::Date(d)),
            Field::CompletionDate => Field::map_or_null(t.completion_date, |d| Value::Date(d)),
            Field::Priority => {
                Field::map_or_null(t.priority, |pri| Value::String_(pri.to_string()))
            }
            Field::CanonicalContext => {
                Field::map_or_null(t.canonical_context(), |c| Value::String_(c))
            }
            Field::CanonicalProject => {
                Field::map_or_null(t.canonical_project(), |c| Value::String_(c))
            }
            Field::Contexts => Value::Set(
                t.contexts
                    .iter()
                    .map(|s| Value::String_(s.to_string()))
                    .collect(),
            ),
            Field::Projects => Value::Set(
                t.contexts
                    .iter()
                    .map(|s| Value::String_(s.to_string()))
                    .collect(),
            ),
            Field::DueDate => Field::map_or_null(t.due_date, |d| Value::Date(d)),
            Field::ThresholdDate => Field::map_or_null(t.due_date, |d| Value::Date(d)),
            Field::IsHidden => Field::bool_to_value(t.is_hidden),
        };
        val
    }
}

#[derive(Eq, PartialEq, Debug)]
enum SelectFields {
    Star,
    // TODO: Fields(HashSet<Field>),
    // TODO: distinct
}

impl Default for SelectFields {
    fn default() -> Self {
        Star
    }
}

#[derive(Eq, PartialEq, Debug)]
enum SelectFrom {
    Stdin,
}

impl Default for SelectFrom {
    fn default() -> Self {
        Stdin
    }
}

#[derive(Default, TypedBuilder, Eq, PartialEq, Debug)]
struct Select {
    #[builder(default)]
    fields: SelectFields,
    #[builder(default)]
    from: SelectFrom,
}

#[derive(Eq, PartialEq, Debug)]
enum ComparisonOperator {
    Equals,
    GreaterThan,
    LessThan,
    Is,
    IsNot,
    In,
    NotIn,
}

impl ComparisonOperator {
    fn matches(&self, v1: &Value, v2: &Value) -> bool {
        let v1null = v1.is_null();
        let v2null = v2.is_null();
        match self {
            ComparisonOperator::Equals => !v1null && !v2null && v1 == v2,
            ComparisonOperator::Is => (v1null && v2null) || v1 == v2,
            _ => unimplemented!(),
        }
    }
}

#[derive(Eq, PartialEq, Debug)]
enum SearchClause {
    Field(Field, ComparisonOperator, Value),
    And(Vec<SearchClause>),
    Or(Vec<SearchClause>),
}

impl SearchClause {
    fn matches(&self, t: &Todo) -> bool {
        match self {
            SearchClause::Field(
                Field::CanonicalContext,
                ComparisonOperator::In,
                Value::Set(haystack),

            ) => match t.canonical_context() {
                None => false,
                Some(c) => haystack.contains(&Value::String_(c)),
            },
            SearchClause::Field(
                Field::CanonicalProject,
                ComparisonOperator::In,
                Value::Set(haystack),
            ) => match t.canonical_project() {
                None => false,
                Some(c) => haystack.contains(&Value::String_(c)),
            },
            SearchClause::Field(f, op, v) => {
                let tv = &f.get_value(t);
                op.matches(tv, v)
            }
            SearchClause::And(clauses) => {
                let mut res = true;
                for c in clauses {
                    if !c.matches(t) {
                        res = false;
                        break;
                    }
                }
                res
            }
            SearchClause::Or(clauses) => {
                let mut res = false;
                for c in clauses {
                    if c.matches(t) {
                        res = true;
                        break;
                    }
                }
                res
            }
        }
    }
}

#[derive(Eq, PartialEq, Debug)]
struct Where {
    search_clause: SearchClause,
}

impl Where {
    fn matches(&self, t: &Todo) -> bool {
        self.search_clause.matches(t)
    }
}

#[derive(Eq, PartialEq, Debug)]
struct Group {
    by: Field,
}

#[derive(Eq, PartialEq, Hash, Debug)]
enum Function {
    ArrayLength,
}

#[derive(Eq, PartialEq, Debug)]
struct Order {
    by: Field,
    desc: bool,
}

#[derive(Default, TypedBuilder, Eq, PartialEq, Debug)]
struct Statement {
    #[builder(default)]
    select: Select,
    #[builder(default, setter(strip_option))]
    where_: Option<Where>,
    #[builder(default, setter(strip_option))]
    group: Option<Group>,
    #[builder(default, setter(strip_option))]
    order: Option<Order>,
}

impl Statement {
    fn matches(&self, t: &Todo) -> bool {
        match &self.where_ {
            None => true,
            Some(where_) => where_.matches(t),
        }
        // unimplemented!()
    }
}

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

    use crate::sql::ComparisonOperator::*;
    use crate::sql::Field::{
        CompletionDate, CreationDate, IsCompleted, IsHidden, Priority, ThresholdDate,
    };
    use crate::sql::Value::{Date, False, Null, String_, True};
    use crate::utils;

    use super::*;

    #[test]
    fn matching_todos_with_empty_where() {
        utils::test::init();

        let t = Todo::parse("x (A) ma vai").unwrap();

        let s = Statement::builder().build();
        assert!(s.matches(&t))
    }

    #[test]
    fn matching_todos_with_field_search_clauses() {
        utils::test::init();

        let mut t = Todo::parse("x (A) 2020-12-25 ma vai t:2020-12-30").unwrap();

        let mut sc = SearchClause::Field(Priority, Equals, String_("A".to_string()));
        assert!(sc.matches(&t));

        sc = SearchClause::Field(Priority, Equals, String_("B".to_string()));
        assert!(!sc.matches(&t));

        sc = SearchClause::Field(IsCompleted, Equals, True);
        assert!(sc.matches(&t));

        sc = SearchClause::Field(CompletionDate, Equals, Null);
        assert!(!sc.matches(&t));

        sc = SearchClause::Field(CompletionDate, Is, Null);
        assert!(sc.matches(&t));

        sc = SearchClause::Field(CreationDate, Equals, Date(time::date!(2020 - 12 - 25)));
        assert!(sc.matches(&t));

        sc = SearchClause::Field(CreationDate, Equals, Date(time::date!(2020 - 12 - 31)));
        assert!(!sc.matches(&t));

        sc = SearchClause::Field(ThresholdDate, Equals, Date(time::date!(2020 - 12 - 30)));
        assert!(!sc.matches(&t));

        sc = SearchClause::Field(IsHidden, Equals, False);
        assert!(sc.matches(&t));
    }

    #[test]
    fn matching_todos_with_and_search_clauses() {
        utils::test::init();
        let mut t = Todo::parse("x (A) 2020-12-25 ma vai t:2020-12-30").unwrap();

        // both criteria match
        let mut sc = SearchClause::And(vec![
            SearchClause::Field(IsHidden, Equals, False),
            SearchClause::Field(IsCompleted, Is, True),
        ]);
        assert!(sc.matches(&t));

        // 2nd criteria doesn't match
        sc = SearchClause::And(vec![
            SearchClause::Field(IsHidden, Equals, False),
            SearchClause::Field(IsCompleted, Is, False),
        ]);
        assert!(!sc.matches(&t));
    }

    #[test]
    fn matching_todos_with_or_search_clauses() {
        utils::test::init();
        let mut t = Todo::parse("x (A) 2020-12-25 ma vai t:2020-12-30").unwrap();

        // 2nd criteria matches
        let mut sc = SearchClause::Or(vec![
            SearchClause::Field(IsCompleted, Equals, False),
            SearchClause::Field(CreationDate, Is, Date(time::date!(2020 - 12 - 25))),
        ]);
        assert!(sc.matches(&t));

        // no criteria matches
        sc = SearchClause::Or(vec![
            SearchClause::Field(IsHidden, Equals, True),
            SearchClause::Field(IsCompleted, Is, False),
        ]);
        assert!(!sc.matches(&t));
    }

    #[test]
    fn matching_todos_with_context_project_in_search_clauses() {}
}