Taskforge

This document describes the goals of Taskforge the library, and it’s design.

Goals

  • Task management library that can be used with multiple frontends
  • Supports saving and loading of tasks from multiple “services” via List implementations
    • MVP: Only supports one list: SQLite
  • Query Language supports both simple string searches and complex field based queries
    • For Example:
      • “milk sugar” will search through all tasks for the words “milk sugar” in the title, body, and notes of tasks
      • “title = WORK” will find tasks whose title is the string WORK

Design

Query Language

The query language for tasks will accept two “modes”.

The first mode is a simple string search. A query which takes the form: WORD^ such as: milk and sugar is a simple string search. It will be a single expression which is a String Literal of “milk and sugar” if an interpreter finds a single String Literal expression as the root node of an AST then it should do a fuzzy string match on the title, body, and notes of Tasks.

The second mode is a tree of infix expressions. An infix expression as the form FIELD_NAME OPERATOR VALUE or, in the case of logical operators, EXPRESSION AND_OR EXPRESSION. As in math and programming languages, the ( and ) characters will denote “grouped” expressions which will push them down the order of operations so they are evaluated first. The final kind of expression is with logical operators. A string literal inside of parens will be treated as an EXPRESSION will be interpreted as a fuzzy search as above so that (STRING_LITERAL) AND_OR EXPRESSION can be used for convenience.

There are three “literal” expressions to represent values:

  • String Literal
  • Number Literal
  • Date Literal

Dates are any string which have the format YYYY-MM-DD HH:MM (AM|PM) whether quoted or unquoted.

Important notes:

  • FIELD_NAME is lexed as a String Literal token. The parser will validate that it is a valid field name if it is part of an infix expression. Else the parser will concatenate multiple FIELD_NAMES into a single String Literal expression.
  • All numbers are lexed as floats, however in a query string both 5 and 5.0 are valid.

Taskforge query language has one “prefix operator” and is not seen by the parser or interpreter (so is not a true operator at all), and that is -. The lexer will use this during tokenization of unquoted strings to change what would normally be a keyword into a string token. Take our above example of milk and sugar. The lexer would normally interpret this as STRING AND STRING. If we instead want this to be taken as STRING STRING STRING we must put a - in front of and. This means the final query is milk -and sugar. The - is simply ignored by anything other than the lexer.

Valid infix operators are:

  • = equality so that title = foo means if title is equal to “foo”
  • != or ^= negative equality so that title != foo means find titles which are not equal to “foo” The != form is preferred but the ! character is troublesome in a shell environment so the ^= form is provided as a convenience.
  • > and >= Greater than and Greater than or equal to so that priority > 5 means priority is greater than 5.0 similarly priority >= 5 simply includes 5.0 as a valid value.
  • < and <= Less than and Less than or equal to. The inverse of the above.
  • ^ or + A “LIKE” operator for strings, performs fuzzy matching instead of strict equality. The + is the preferred form however is inconvenient for terminal use so ^ is also valid.
  • AND or and both the upper and lower case forms of and are acceptable. These perform a logical and of two expressions.
  • OR or or both the upper and lower case forms of or are acceptable. These perform a logical or of two expressions.

Some example queries with literate explanations of interpreter behavior:

  • title = "take out the trash"
    • Find all tasks which have the title “take out the trash”
  • title ^ "take out the trash"
    • Find all tasks whose title contains the string “take out the trash”
  • ("milk and sugar") and priority > 5
    • Find all tasks which have the string “milk and sugar” fuzzy matched on their title, body, and notes. Additionally verify that they have a priority greater than 5.0
  • milk -and sugar
    • Find all tasks which have the string “milk and sugar” fuzzy matched on their title, body, and notes.
  • (priority > 5 and title ^ "take out the trash") or (context = "work" and (priority >= 2 or ("my little pony")))
    • Find all tasks which either have a priority greater than 5.0 and a title containing the string “take out the trash” or which are in the context “work” and have a priority greater than or equal to 2 or have the string “my little pony” in their title, body, and notes.

AST

The AST for the query language returned by the parser is a struct which currently has a single member expression.

/// The AST is the primary way a list will interact with Taskforge Queries.
///
/// Documentation about AST's is beyond the scope of this
/// document. For information and an example of writing a Taskforge
/// Query Language compiler see:
///
/// [Implementing a Taskforge List](/docs/development/building_a_list.html#Writing-A-Compiler)
#[derive(Debug, Clone, PartialEq)]
pub struct AST {
       pub expression: Expression,
}

expression is an enumeration representing all of the values an expression can yield. It’s definition is as follows:

/// Expression is an enum representing a value parsed by the TFQL
/// parser.
#[derive(Debug, Clone, PartialEq)]
pub enum Expression {
       String(String),
       Number(f64),
       Date(DateTime<Local>),
       Bool(bool),

       Invalid(String),

       Infix(Box<Expression>, Operator, Box<Expression>),
}

Task Data

The pseudo-code representation of a task is:

{
    id: String,
    title: String,
    context: String
    created_date: Date,
    completed_date: Date | null,
    body: String,
    priority: Float,
    notes: [Note]
}

A Note will be represented as:

{
    id: String,
    created_date: Date,
    body: String,
}

All ID’s will be hex strings of BSON ObjectIDs regardless of list storage. This is a nice, stand alone, and easy to use UUID that can be made into a string.

Task Lists

List is a trait that is implemented by any struct which can store and retrieve tasks. Taskforge clients should primarily work with Tasks through a List implementation. The trait is defined as:

/// List is implemented by any struct that can maintain tasks
pub trait List {
       /// Add a task to the List
       fn add(&mut self, task: Task) -> Result<(), Error>;

       /// Add multiple tasks to the List, should be more efficient resource
       /// utilization.
       fn add_multiple(&mut self, task: Vec<Task>) -> Result<(), Error>;

       /// Find a task by ID
       fn find_by_id(&mut self, id: String) -> Result<Task, Error>;

       /// Return the current task, meaning the oldest uncompleted task in
       /// the List
       fn current(&mut self) -> Result<Task, Error>;

       /// Return the contexts used in this list
       fn contexts(&mut self) -> Result<Vec<String>, Error>;

       /// Complete a task by id
       fn complete(&mut self, id: String) -> Result<(), Error>;

       /// Update a task in the list, finding the original by the ID of
       /// the given task
       fn update(&mut self, task: Task) -> Result<(), Error>;

       /// Add note to a task by ID
       fn add_note(&mut self, id: String, note: Note) -> Result<(), Error>;

       /// Interpret the ast and search for tasks which match
       fn search(&mut self, ast: AST) -> Result<Vec<Task>, Error>;
}

Future Work / Ideas

Future ideas and features I will implement are as follows:

  • Additional Lists:

    • Postgres
    • GitHub
    • Gitlab
    • Trello
  • GUI Frontends

  • Modifier statements on queries such as LIMIT or ORDER BY

  • Task custom fields