Просмотр исходного кода

tau: rework on tau-cli functionalities and design

ghassmo 4 лет назад
Родитель
Сommit
7796f313ef

+ 55 - 159
bin/tau/README.md

@@ -32,16 +32,6 @@ seed node shouldn't be advertised in the list of connectable nodes. The seed
 node does not participate as a normal node in the p2p network. It simply allows
 node does not participate as a normal node in the p2p network. It simply allows
 new nodes to discover other nodes in the network during the bootstrapping phase.
 new nodes to discover other nodes in the network during the bootstrapping phase.
 
 
-Also note that for the first time ever running seed node you must run it with 
-`--key-gen`:
-```shell
-% taud --key-gen
-```
-This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that 
-you can share with nodes you want them to get and decrypt your tasks, otherwise if you
-have already generated or got a copy from a peer place it in the same directory
-`/home/${USER}/.config/tau/secret_key`.
-
 ### Inbound Node
 ### Inbound Node
 
 
 This is a node accepting inbound connections on the network but which is not
 This is a node accepting inbound connections on the network but which is not
@@ -74,175 +64,81 @@ connect to in the p2p network.
 		## Seed nodes to connect to 
 		## Seed nodes to connect to 
 		seeds=["127.0.0.1:11001"]
 		seeds=["127.0.0.1:11001"]
 
 
+
+Also note that for the first time ever running seed node you must run it with 
+`--key-gen`:
+```shell
+% taud --key-gen
+```
+This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that 
+you can share with nodes you want them to get and decrypt your tasks, otherwise if you
+have already generated or got a copy from a peer place it in the same directory
+`/home/${USER}/.config/tau/secret_key`.
+
+
 ## Usage (CLI)
 ## Usage (CLI)
 
 
 ```shell
 ```shell
 % tau --help 
 % tau --help 
 ```
 ```
-
 	tau 0.3.0
 	tau 0.3.0
 	Tau cli
 	Tau cli
-
+	
 	USAGE:
 	USAGE:
-		tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
-
+	    tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
+	
 	FLAGS:
 	FLAGS:
-		-h, --help       Prints help information
-		-V, --version    Prints version information
-		-v               Increase verbosity
-
+	    -h, --help       Prints help information
+	    -V, --version    Prints version information
+	    -v               Increase verbosity
+	
 	OPTIONS:
 	OPTIONS:
-		-c, --config <config>     Sets a custom config file
-			--rpc <rpc-listen>    JSON-RPC listen URL [default: 127.0.0.1:11055]
-
+	    -c, --config <config>     Sets a custom config file
+	        --rpc <rpc-listen>    JSON-RPC listen URL [default: 127.0.0.1:11055]
+	
 	ARGS:
 	ARGS:
-		<id>            Get task by ID
-		<filters>...    Search criteria (zero or more)
-
+	    <id>            Get task by ID
+	    <filters>...    Search criteria (zero or more)
+	
 	SUBCOMMANDS:
 	SUBCOMMANDS:
-		add        Add a new task
-		comment    Set or Get comment for a task
-		help       Prints this message or the help of the given subcommand(s)
-		list       List all tasks
-		state      Set or Get task state
-		update     Update/Edit an existing task by ID
+	    add        Add a new task
+	    comment    Set or Get comment for a task
+	    help       Prints this message or the help of the given subcommand(s)
+	    list       List all tasks
+	    state      Set or Get task state
+	    update     Update/Edit an existing task by ID
 
 
 ```shell
 ```shell
 % tau help [SUBCOMMAND]
 % tau help [SUBCOMMAND]
 ```
 ```
 
 
-### Add new tasks
-
-```shell
-% tau add title1 description person1,person2 project1,project2 0405 4.74
-% tau add title2 "some description" person1 project1 0805 18
-% # this will prompt terminal for title
-% tau add
-Title: new title
-% # then your system's default editor will open up and you could write some description
-% # you should have/add environment variable EDITOR pointing to your favorite text editor
-```
-for more information:
-```shell
-% tau add --help
-```
-
-
-### List existing tasks
-
-```shell
-% tau list # or just tau
-```
-Output:
-```text
- ID | Title     | Project           | Assigned        | Due             | Rank 
-----+-----------+-------------------+-----------------+-----------------+------
- 2  | title2    | project1          | person1         | Sunday 8 May    | 18 
- 1  | title1    | project1,project2 | person1,person2 | Wednesday 4 May | 4.74 
- 3  | new title |                   |                 |                 | 0 
-```
-
-
-### List tasks with filters
-
-```shell
-% tau  		   		 # lists all tasks
-% tau open 			 # lists currently open tasks
-% tau pause 		 # lists currently paused tasks
-% tau 0522 		 	 # lists tasks created at May 2022
-% tau project:value  # lists all tasks that have "value" in their Project
-% tau assign:value   # lists all tasks that have "value" in their Assign
-% tau "rank>number"  # lists all tasks that have rank greater than "number"
-% tau "rank<number"  # lists all tasks that have rank lesser than "number"
-```
-
-Combined filters:
-```shell
-% tau project:project1 assign:person2 0522 open
-```
-Output:
-```text
- ID | Title  | Project  | Assigned        | Due             | Rank 
-----+--------+----------+-----------------+-----------------+------
- 1  | title1 | project1 | person1,person2 | Wednesday 4 May | 4.74 
-```
-
-
-### Update an existing task
-
-```shell
-% tau update 3 project project3 
-% tau "rank<4" # qoutes are for escaping special characters
-```
-Output:
-```text
- ID | Title     | Project  | Assigned | Due | Rank 
-----+-----------+----------+----------+-----+------
- 3  | new title | project3 |          |     | 0 
-```
-
-
-### Get/Set task state
-
-```shell
-% tau state 1 
-```
-Output:
-```text
-Task with id 1 is: "open"
-```
-
-```shell
-% tau state 1 pause
-% tau state 1 
-```
-Output:
-```text
-Task with id 1 is: "pause"
-```
-
-```shell
-% tau state 2 stop # this will deactivate the task (task is done)
-```
-
-
-### Get/Set comment
+### Example  
 
 
 ```shell
 ```shell
-% tau comment 1 person1 "some awesome comment"
-% tau comment 1 person2 "other awesome comment"
-% tau comment 1
-```
-Output:
-```text
-Comments on Task with id 1:
-person1: some awesome comment
-person2: other awesome comment
+$ # add new task  
+$ tau add "new title"   
+$ tau add "new title" project:blockchain desc:"new description" rank:3 assign:dark
+$
+$ # lists tasks
+$ tau  		   		 
+$ tau open 			 # open tasks
+$ tau pause 		 # paused tasks
+$ tau 0522 		 	 # created at May 2022
+$ tau project:blockchain assign:dark
+$ tau rank:gt:n  # lists all tasks that have rank greater than n
+$ tau rank:ls:n  # lists all tasks that have rank lesser than n
+$
+$ # update task 
+$ tau update 3 project:network  rank:20
+$
+$ # state 
+$ tau state 3  # get state
+$ tau state 3 pause  # set the state to pause 
+$
+$ # comments 
+$ tau comments 1  # list comments
+$ tau comments 3 "new comment"  # add new comment 
 ```
 ```
 
 
 
 
-### Get a task
 
 
-```shell
-% tau 1
-```
-Output:
-```text
- Name          | Value 
----------------+--------------------------------
- ref_id        | cGw1AI7cBSdJWIqPMU8d355wRrB0qy 
- id            | 1 
- title         | title1 
- desc          | description 
- assign        | person1,person2 
- project       | project1 
- due           | Wednesday 4 May 
- rank          | 4.74 
- created_at    | 21:28 Monday 2 May 
- current_state | pause 
- comments      | person1: some awesome comment 
-               | person2: other awesome comment 
-------------------------------------------------------
- events  State changed to pause at 21:34 Monday 2 May 
-------------------------------------------------------
-```

+ 112 - 0
bin/tau/tau-cli/src/cli.rs

@@ -0,0 +1,112 @@
+use std::net::SocketAddr;
+
+use serde::{Deserialize, Serialize};
+use structopt::StructOpt;
+use structopt_toml::StructOptToml;
+
+use darkfi::Result;
+
+use super::util::due_as_timestamp;
+
+/// Tau cli
+#[derive(Debug, Deserialize, StructOpt, StructOptToml)]
+#[serde(default)]
+#[structopt(name = "tau")]
+pub struct CliTau {
+    /// Increase verbosity
+    #[structopt(short, parse(from_occurrences))]
+    pub verbose: u8,
+    /// JSON-RPC listen URL
+    #[structopt(long = "rpc", default_value = "127.0.0.1:11055")]
+    pub rpc_listen: SocketAddr,
+    /// Sets a custom config file
+    #[structopt(short, long)]
+    pub config: Option<String>,
+    #[structopt(subcommand)]
+    pub command: Option<CliTauSubCommands>,
+    /// Get task by ID
+    pub id: Option<String>,
+    /// Search criteria (zero or more)
+    #[structopt(multiple = true)]
+    pub filters: Vec<String>,
+}
+
+#[derive(StructOpt, Deserialize, Debug)]
+pub enum CliTauSubCommands {
+    /// Add a new task
+    Add { values: Vec<String> },
+    /// Update/Edit an existing task by ID
+    Update {
+        /// Task ID
+        id: u64,
+        /// Values (ex: project:blockchain)
+        values: Vec<String>,
+    },
+    /// Set or Get task state
+    State {
+        /// Task ID
+        id: u64,
+        /// Set task state
+        state: Option<String>,
+    },
+    /// Set or Get comment for a task
+    Comment {
+        /// Task ID
+        id: u64,
+        /// Comment content
+        content: Option<String>,
+    },
+    /// List all tasks
+    List {},
+}
+
+#[derive(Serialize, Debug, Clone)]
+pub struct CliBaseTask {
+    pub title: String,
+    pub desc: Option<String>,
+    pub assign: Vec<String>,
+    pub project: Vec<String>,
+    pub due: Option<i64>,
+    pub rank: Option<f32>,
+}
+
+pub fn task_from_cli_values(values: Vec<String>) -> Result<CliBaseTask> {
+    let mut title: String = String::new();
+    let mut desc: Option<String> = None;
+    let mut project: Vec<String> = vec![];
+    let mut assign: Vec<String> = vec![];
+    let mut due: Option<i64> = None;
+    let mut rank: Option<f32> = None;
+
+    for val in values {
+        let field: Vec<&str> = val.split(':').collect();
+        if field.len() == 1 {
+            title = field[0].into();
+            continue
+        }
+
+        if field.len() != 2 {
+            continue
+        }
+
+        if field[0].starts_with("project") {
+            project.push(field[1].into());
+        }
+        if field[0].starts_with("desc") {
+            desc = Some(field[1].into());
+        }
+        if field[0].starts_with("assign") {
+            assign.push(field[1].into());
+        }
+        if field[0].starts_with("due") {
+            due = due_as_timestamp(&field[1])
+        }
+        if field[0].starts_with("rank") {
+            rank = Some(field[1].parse::<f32>()?);
+        }
+    }
+
+    let task = CliBaseTask { title, desc, project, assign, due, rank };
+
+    Ok(task)
+}

+ 71 - 0
bin/tau/tau-cli/src/filter.rs

@@ -0,0 +1,71 @@
+use chrono::{Datelike, NaiveDate, NaiveDateTime};
+use serde_json::Value;
+
+use super::primitives::{TaskEvent, TaskInfo};
+
+// Helper function to check task's state
+fn check_task_state(task: &TaskInfo, state: &str) -> bool {
+    let last_state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
+    state == last_state
+}
+
+pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
+    match filter.as_str() {
+        "open" => tasks.retain(|task| check_task_state(task, "open")),
+        "pause" => tasks.retain(|task| check_task_state(task, "pause")),
+
+        _ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
+            let (month, year) =
+                (filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().unwrap());
+
+            let year = year + 2000;
+
+            tasks.retain(|task| {
+                let date = task.created_at;
+                let task_date = NaiveDateTime::from_timestamp(date, 0).date();
+                let filter_date = NaiveDate::from_ymd(year, month, 1);
+                task_date.month() == filter_date.month() && task_date.year() == filter_date.year()
+            })
+        }
+
+        _ if filter.contains("assign:") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+            if kv.len() == 2 {
+                let value = Value::from(kv[1]);
+                if value.as_str().is_some() {
+                    tasks.retain(|task| task.assign.contains(&value.as_str().unwrap().into()))
+                }
+            }
+        }
+
+        _ if filter.contains("project:") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+            if kv.len() == 2 {
+                let value = Value::from(kv[1]);
+                if value.as_str().is_some() {
+                    tasks.retain(|task| task.project.contains(&value.as_str().unwrap().into()))
+                }
+            }
+        }
+
+        _ if filter.contains("rank:") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+
+            if kv.len() == 3 {
+                let value = kv[2].parse::<f32>().unwrap_or(0.0);
+
+                tasks.retain(|task| {
+                    if filter.contains("lt") {
+                        task.rank < value
+                    } else if filter.contains("gt") {
+                        task.rank > value
+                    } else {
+                        true
+                    }
+                })
+            }
+        }
+
+        _ => {}
+    };
+}

+ 3 - 11
bin/tau/tau-cli/src/jsonrpc.rs

@@ -75,14 +75,6 @@ pub async fn set_state(url: &str, id: u64, state: &str) -> Result<Value> {
     request(req, url.to_string()).await
     request(req, url.to_string()).await
 }
 }
 
 
-// Get task's state.
-// --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
-// <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
-pub async fn get_state(url: &str, id: u64) -> Result<Value> {
-    let req = jsonrpc::request(json!("get_state"), json!([id]));
-    request(req, url.to_string()).await
-}
-
 // Set comment for a task and returns `true` upon success.
 // Set comment for a task and returns `true` upon success.
 // --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_content], "id": 1}
 // --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_content], "id": 1}
 // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
 // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
@@ -92,9 +84,9 @@ pub async fn set_comment(url: &str, id: u64, content: &str) -> Result<Value> {
 }
 }
 
 
 // Get task by id.
 // Get task by id.
-// --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1}
+// --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1}
 // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
 // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
-pub async fn get_by_id(url: &str, id: u64) -> Result<Value> {
-    let req = jsonrpc::request(json!("get_by_id"), json!([id]));
+pub async fn get_task_by_id(url: &str, id: u64) -> Result<Value> {
+    let req = jsonrpc::request(json!("get_task_by_id"), json!([id]));
     request(req, url.to_string()).await
     request(req, url.to_string()).await
 }
 }

+ 43 - 77
bin/tau/tau-cli/src/main.rs

@@ -1,6 +1,7 @@
 use log::error;
 use log::error;
-use serde_json::{json, Value};
+use serde_json::json;
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
+use structopt_toml::StructOptToml;
 
 
 use darkfi::{
 use darkfi::{
     util::{
     util::{
@@ -9,29 +10,29 @@ use darkfi::{
     },
     },
     Result,
     Result,
 };
 };
-use structopt_toml::StructOptToml;
 
 
+mod cli;
+mod filter;
 mod jsonrpc;
 mod jsonrpc;
+mod primitives;
 mod util;
 mod util;
+mod view;
 
 
-use crate::{
-    jsonrpc::{add, get_by_id, get_state, list, set_comment, set_state, update},
-    util::{
-        desc_in_editor, due_as_timestamp, get_comments, list_tasks, set_title, show_task, CliTau,
-        CliTauSubCommands, TaskInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS,
-    },
-};
+use cli::CliTauSubCommands;
+use primitives::TaskInfo;
+use util::{desc_in_editor, CONFIG_FILE, CONFIG_FILE_CONTENTS};
+use view::{comments_as_string, events_as_string, print_list_of_task, print_task_info};
 
 
-async fn start(mut options: CliTau) -> Result<()> {
+async fn start(mut options: cli::CliTau) -> Result<()> {
     let rpc_addr = &format!("tcp://{}", &options.rpc_listen.clone());
     let rpc_addr = &format!("tcp://{}", &options.rpc_listen.clone());
 
 
+    let states: Vec<String> = vec!["stop".into(), "open".into(), "pause".into()];
+
     match options.id {
     match options.id {
         Some(id) if id.len() < 4 && id.parse::<u64>().is_ok() => {
         Some(id) if id.len() < 4 && id.parse::<u64>().is_ok() => {
-            let task = get_by_id(rpc_addr, id.parse::<u64>().unwrap()).await?;
+            let task = jsonrpc::get_task_by_id(rpc_addr, id.parse::<u64>().unwrap()).await?;
             let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
             let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
-            let current_state: String =
-                serde_json::from_value(get_state(rpc_addr, id.parse::<u64>().unwrap()).await?)?;
-            show_task(task, taskinfo, current_state)?;
+            print_task_info(taskinfo)?;
             return Ok(())
             return Ok(())
         }
         }
         Some(id) => options.filters.push(id),
         Some(id) => options.filters.push(id),
@@ -39,93 +40,58 @@ async fn start(mut options: CliTau) -> Result<()> {
     }
     }
 
 
     match options.command {
     match options.command {
-        Some(CliTauSubCommands::Add { title, desc, assign, project, due, rank }) => {
-            let title = match title {
-                Some(t) => t,
-                None => set_title()?,
+        Some(CliTauSubCommands::Add { values }) => {
+            let mut task = cli::task_from_cli_values(values)?;
+            if task.title.is_empty() {
+                error!("Provide a title for the task");
+                return Ok(())
             };
             };
 
 
-            let desc = match desc {
-                Some(d) => Some(d),
-                None => desc_in_editor()?,
+            if task.desc.is_none() {
+                task.desc = desc_in_editor()?;
             };
             };
 
 
-            let assign: Vec<String> = match assign {
-                Some(a) => a.split(',').map(|s| s.into()).collect(),
-                None => vec![],
-            };
-
-            let project: Vec<String> = match project {
-                Some(p) => p.split(',').map(|s| s.into()).collect(),
-                None => vec![],
-            };
-
-            let due = match due {
-                Some(d) => due_as_timestamp(&d),
-                None => None,
-            };
-
-            let rank = rank.unwrap_or(0.0);
-
-            add(rpc_addr,
-                json!([{"title": title, "desc": desc, "assign": assign, "project": project, "due": due, "rank": rank}]),
-                ).await?;
+            jsonrpc::add(rpc_addr, json!([task])).await?;
         }
         }
 
 
-        Some(CliTauSubCommands::Update { id, key, value }) => {
-            let value = value.as_str().trim();
-
-            let updated_value: Value = match key.as_str() {
-                "due" => {
-                    json!(due_as_timestamp(value))
-                }
-                "rank" => {
-                    json!(value.parse::<f32>()?)
-                }
-                "project" | "assign" => {
-                    json!(value.split(',').collect::<Vec<&str>>())
-                }
-                _ => {
-                    json!(value)
-                }
-            };
-
-            update(rpc_addr, id, json!({ key: updated_value })).await?;
+        Some(CliTauSubCommands::Update { id, values }) => {
+            let task = cli::task_from_cli_values(values)?;
+            jsonrpc::update(rpc_addr, id, json!([task])).await?;
         }
         }
 
 
         Some(CliTauSubCommands::State { id, state }) => match state {
         Some(CliTauSubCommands::State { id, state }) => match state {
             Some(state) => {
             Some(state) => {
-                if state.as_str() == "open" {
-                    set_state(rpc_addr, id, state.trim()).await?;
-                } else if state.as_str() == "pause" {
-                    set_state(rpc_addr, id, state.trim()).await?;
-                } else if state.as_str() == "stop" {
-                    set_state(rpc_addr, id, state.trim()).await?;
+                let state = state.trim().to_lowercase();
+                if states.contains(&state) {
+                    jsonrpc::set_state(rpc_addr, id, &state).await?;
                 } else {
                 } else {
                     error!("Task state could only be one of three states: open, pause or stop");
                     error!("Task state could only be one of three states: open, pause or stop");
                 }
                 }
             }
             }
             None => {
             None => {
-                let state = get_state(rpc_addr, id).await?;
-                println!("Task with id {} is: {}", id, state);
+                let task = jsonrpc::get_task_by_id(rpc_addr, id).await?;
+                let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
+                let state = events_as_string(taskinfo.events);
+                println!("Task {}: {}", id, state);
             }
             }
         },
         },
 
 
         Some(CliTauSubCommands::Comment { id, content }) => match content {
         Some(CliTauSubCommands::Comment { id, content }) => match content {
             Some(content) => {
             Some(content) => {
-                set_comment(rpc_addr, id, content.trim()).await?;
+                jsonrpc::set_comment(rpc_addr, id, content.trim()).await?;
             }
             }
             None => {
             None => {
-                let rep = get_by_id(rpc_addr, id).await?;
-                let comments = get_comments(rep)?;
-
-                println!("Comments on Task with id {}:\n{}", id, comments);
+                let task = jsonrpc::get_task_by_id(rpc_addr, id).await?;
+                let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
+                let comments = comments_as_string(taskinfo.comments);
+                println!("Comments {}:\n{}", id, comments);
             }
             }
         },
         },
 
 
         Some(CliTauSubCommands::List {}) | None => {
         Some(CliTauSubCommands::List {}) | None => {
-            let rep = list(rpc_addr, json!([])).await?;
-            list_tasks(rep, options.filters)?;
+            let tasks = jsonrpc::list(rpc_addr, json!([])).await?;
+            let mut tasks: Vec<TaskInfo> = serde_json::from_value(tasks)?;
+            print_list_of_task(&mut tasks, options.filters)?;
         }
         }
     }
     }
 
 
@@ -134,10 +100,10 @@ async fn start(mut options: CliTau) -> Result<()> {
 
 
 #[async_std::main]
 #[async_std::main]
 async fn main() -> Result<()> {
 async fn main() -> Result<()> {
-    let args = CliTau::from_args_with_toml("").unwrap();
+    let args = cli::CliTau::from_args_with_toml("").unwrap();
     let cfg_path = get_config_path(args.config, CONFIG_FILE)?;
     let cfg_path = get_config_path(args.config, CONFIG_FILE)?;
     spawn_config(&cfg_path, CONFIG_FILE_CONTENTS.as_bytes())?;
     spawn_config(&cfg_path, CONFIG_FILE_CONTENTS.as_bytes())?;
-    let args = CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap();
+    let args = cli::CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap();
 
 
     let (lvl, conf) = log_config(args.verbose.into())?;
     let (lvl, conf) = log_config(args.verbose.into())?;
     TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
     TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;

+ 63 - 0
bin/tau/tau-cli/src/primitives.rs

@@ -0,0 +1,63 @@
+use serde::{Deserialize, Serialize};
+
+use super::util::timestamp_to_date;
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct TaskInfo {
+    pub ref_id: String,
+    pub id: u32,
+    pub title: String,
+    pub desc: String,
+    pub owner: String,
+    pub assign: Vec<String>,
+    pub project: Vec<String>,
+    pub due: Option<i64>,
+    pub rank: f32,
+    pub created_at: i64,
+    pub events: Vec<TaskEvent>,
+    pub comments: Vec<Comment>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct TaskEvent {
+    pub action: String,
+    pub timestamp: Timestamp,
+}
+
+impl std::fmt::Display for TaskEvent {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "action: {}, timestamp: {}", self.action, self.timestamp)
+    }
+}
+
+impl Default for TaskEvent {
+    fn default() -> Self {
+        TaskEvent {
+            action: "open".to_string(),
+            timestamp: Timestamp(chrono::offset::Local::now().timestamp()),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Comment {
+    content: String,
+    author: String,
+    timestamp: Timestamp,
+}
+
+impl std::fmt::Display for Comment {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{} author: {}, content: {} ", self.timestamp, self.author, self.content)
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Timestamp(pub i64);
+
+impl std::fmt::Display for Timestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        let date = timestamp_to_date(self.0, "datetime");
+        write!(f, "{}", date)
+    }
+}

+ 2 - 310
bin/tau/tau-cli/src/util.rs

@@ -1,109 +1,19 @@
 use std::{
 use std::{
     env::{temp_dir, var},
     env::{temp_dir, var},
     fs::{self, File},
     fs::{self, File},
-    io::{self, Read, Write},
-    net::SocketAddr,
-    ops::Index,
+    io::Read,
     process::Command,
     process::Command,
 };
 };
 
 
 use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
 use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
 use log::error;
 use log::error;
-use prettytable::{cell, format, row, table, Cell, Row, Table};
 use rand::distributions::{Alphanumeric, DistString};
 use rand::distributions::{Alphanumeric, DistString};
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
 
 
 use darkfi::{Error, Result};
 use darkfi::{Error, Result};
-use structopt::StructOpt;
-use structopt_toml::StructOptToml;
 
 
 pub const CONFIG_FILE: &str = "taud_config.toml";
 pub const CONFIG_FILE: &str = "taud_config.toml";
 pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml");
 pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml");
 
 
-#[derive(StructOpt, Deserialize, Debug)]
-pub enum CliTauSubCommands {
-    /// Add a new task
-    Add {
-        /// Specify task title
-        title: Option<String>,
-        /// Specify task description
-        desc: Option<String>,
-        /// Assign task to user
-        assign: Option<String>,
-        /// Task project (can be hierarchical: crypto.zk)
-        project: Option<String>,
-        /// Due date in DDMM format: "2202" for 22 Feb
-        due: Option<String>,
-        /// Project rank single precision decimal real value: 4.8761
-        rank: Option<f32>,
-    },
-    /// Update/Edit an existing task by ID
-    Update {
-        /// Task ID
-        id: u64,
-        /// Field's name (ex title)
-        key: String,
-        /// New value
-        value: String,
-    },
-    /// Set or Get task state
-    State {
-        /// Task ID
-        id: u64,
-        /// Set task state
-        state: Option<String>,
-    },
-    /// Set or Get comment for a task
-    Comment {
-        /// Task ID
-        id: u64,
-        /// Comment content
-        content: Option<String>,
-    },
-    /// List all tasks
-    List {},
-}
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct TaskInfo {
-    pub ref_id: String,
-    pub id: u32,
-    pub title: String,
-    pub desc: String,
-    pub owner: String,
-    pub assign: Vec<String>,
-    pub project: Vec<String>,
-    pub due: Option<i64>,
-    pub rank: f32,
-    pub created_at: i64,
-    pub events: Vec<Value>,
-    pub comments: Vec<Value>,
-}
-
-/// Tau cli
-#[derive(Debug, Deserialize, StructOpt, StructOptToml)]
-#[serde(default)]
-#[structopt(name = "tau")]
-pub struct CliTau {
-    /// Increase verbosity
-    #[structopt(short, parse(from_occurrences))]
-    pub verbose: u8,
-    /// JSON-RPC listen URL
-    #[structopt(long = "rpc", default_value = "127.0.0.1:11055")]
-    pub rpc_listen: SocketAddr,
-    /// Sets a custom config file
-    #[structopt(short, long)]
-    pub config: Option<String>,
-    #[structopt(subcommand)]
-    pub command: Option<CliTauSubCommands>,
-    /// Get task by ID
-    pub id: Option<String>,
-    /// Search criteria (zero or more)
-    #[structopt(multiple = true)]
-    pub filters: Vec<String>,
-}
-
 pub fn due_as_timestamp(due: &str) -> Option<i64> {
 pub fn due_as_timestamp(due: &str) -> Option<i64> {
     if due.len() == 4 {
     if due.len() == 4 {
         let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
         let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
@@ -130,24 +40,6 @@ pub fn due_as_timestamp(due: &str) -> Option<i64> {
     None
     None
 }
 }
 
 
-pub fn set_title() -> Result<String> {
-    print!("Title: ");
-    io::stdout().flush()?;
-    let mut t = String::new();
-    io::stdin().read_line(&mut t)?;
-
-    if t.is_empty() {
-        error!("You can't have a task without a title");
-        return Err(Error::OperationFailed)
-    }
-
-    if &t[(t.len() - 1)..] == "\n" {
-        t.pop();
-    }
-
-    Ok(t)
-}
-
 pub fn desc_in_editor() -> Result<Option<String>> {
 pub fn desc_in_editor() -> Result<Option<String>> {
     // Create a temporary file with some comments inside
     // Create a temporary file with some comments inside
     let mut file_path = temp_dir();
     let mut file_path = temp_dir();
@@ -186,71 +78,7 @@ pub fn desc_in_editor() -> Result<Option<String>> {
     Ok(Some(description))
     Ok(Some(description))
 }
 }
 
 
-pub fn show_task(task: Value, taskinfo: TaskInfo, current_state: String) -> Result<()> {
-    let mut table = table!([Bd => "ref_id", &taskinfo.ref_id],
-                                            ["id", &taskinfo.id.to_string()],
-                                            [Bd =>"owner", &taskinfo.owner],
-                                            [Bd =>"title", &taskinfo.title],
-                                            ["desc", &taskinfo.desc],
-                                            [Bd =>"assign", get_from_task(task.clone(), "assign")?],
-                                            ["project", get_from_task(task.clone(), "project")?],
-                                            [Bd =>"due", timestamp_to_date(task["due"].clone(),"date")],
-                                            ["rank", &taskinfo.rank.to_string()],
-                                            [Bd =>"created_at", timestamp_to_date(task["created_at"].clone(), "datetime")],
-                                            ["current_state", &current_state],
-                                            [Bd => "comments", get_comments(task.clone())?]);
-
-    table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
-    table.set_titles(row!["Name", "Value"]);
-
-    table.printstd();
-
-    let mut event_table = table!(["events", get_events(task)?]);
-    event_table.set_format(*format::consts::FORMAT_NO_COLSEP);
-
-    event_table.printstd();
-
-    Ok(())
-}
-
-pub fn get_comments(rep: Value) -> Result<String> {
-    let task: Value = serde_json::from_value(rep)?;
-
-    let comments: Vec<Value> = serde_json::from_value(task["comments"].clone())?;
-    let mut result = String::new();
-
-    for comment in comments {
-        result.push_str(comment["author"].as_str().ok_or(Error::OperationFailed)?);
-        result.push_str(": ");
-        result.push_str(comment["content"].as_str().ok_or(Error::OperationFailed)?);
-        result.push('\n');
-    }
-    result.pop();
-
-    Ok(result)
-}
-
-pub fn get_events(rep: Value) -> Result<String> {
-    let task: Value = serde_json::from_value(rep)?;
-
-    let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
-    let mut ev = String::new();
-
-    for event in events {
-        ev.push_str("State changed to ");
-        ev.push_str(event["action"].as_str().ok_or(Error::OperationFailed)?);
-        ev.push_str(" at ");
-        ev.push_str(&timestamp_to_date(event["timestamp"].clone(), "datetime"));
-        ev.push('\n');
-    }
-    ev.pop();
-
-    Ok(ev)
-}
-
-pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
-    let timestamp = timestamp.as_i64().unwrap_or(0);
-
+pub fn timestamp_to_date(timestamp: i64, dt: &str) -> String {
     if timestamp <= 0 {
     if timestamp <= 0 {
         return "".to_string()
         return "".to_string()
     }
     }
@@ -265,139 +93,3 @@ pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
         _ => "".to_string(),
         _ => "".to_string(),
     }
     }
 }
 }
-
-pub fn get_from_task(task: Value, value: &str) -> Result<String> {
-    let vec_values: Vec<Value> = serde_json::from_value(task[value].clone())?;
-    let mut result = String::new();
-    for (i, _) in vec_values.iter().enumerate() {
-        if !result.is_empty() {
-            result.push(',');
-        }
-        result.push_str(vec_values.index(i).as_str().unwrap());
-    }
-    Ok(result)
-}
-
-// Helper function to check task's state
-fn check_task_state(task: &Value, state: &str) -> bool {
-    let events = match task["events"].as_array() {
-        Some(t) => t.to_owned(),
-        None => {
-            error!("Value is not an array!");
-            vec![]
-        }
-    };
-
-    let last_state = match events.last() {
-        Some(s) => s["action"].as_str().unwrap(),
-        None => "open",
-    };
-    state == last_state
-}
-
-fn apply_filter(tasks: &mut Vec<Value>, filter: String) {
-    match filter.as_str() {
-        "open" => tasks.retain(|task| check_task_state(task, "open")),
-        "pause" => tasks.retain(|task| check_task_state(task, "pause")),
-
-        _ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
-            let (month, year) =
-                (filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().unwrap());
-
-            let year = year + 2000;
-
-            tasks.retain(|task| {
-                let date = task["created_at"].as_i64().unwrap();
-                let task_date = NaiveDateTime::from_timestamp(date, 0).date();
-                let filter_date = NaiveDate::from_ymd(year, month, 1);
-                task_date.month() == filter_date.month() && task_date.year() == filter_date.year()
-            })
-        }
-
-        _ if filter.contains("assign:") | filter.contains("project:") => {
-            let kv: Vec<&str> = filter.split(':').collect();
-            let key = kv[0];
-            let value = Value::from(kv[1]);
-
-            tasks.retain(|task| task[key].as_array().unwrap_or(&vec![]).contains(&value))
-        }
-
-        _ if filter.contains("rank>") | filter.contains("rank<") => {
-            let kv: Vec<&str> = if filter.contains('>') {
-                filter.split('>').collect()
-            } else {
-                filter.split('<').collect()
-            };
-            let key = kv[0];
-            let value = kv[1].parse::<f32>().unwrap_or(0.0);
-
-            tasks.retain(|task| {
-                let rank = task[key].as_f64().unwrap_or(0.0) as f32;
-                if filter.contains('>') {
-                    rank > value
-                } else {
-                    rank < value
-                }
-            })
-        }
-
-        _ => {}
-    };
-}
-
-pub fn list_tasks(rep: Value, filters: Vec<String>) -> Result<()> {
-    let mut table = Table::new();
-    table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
-    table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
-
-    let mut tasks: Vec<Value> = serde_json::from_value(rep)?;
-
-    for filter in filters {
-        apply_filter(&mut tasks, filter);
-    }
-
-    tasks.sort_by(|a, b| b["rank"].as_f64().partial_cmp(&a["rank"].as_f64()).unwrap());
-
-    let (max_rank, min_rank) = if !tasks.is_empty() {
-        (
-            serde_json::from_value(tasks[0]["rank"].clone())?,
-            serde_json::from_value(tasks[tasks.len() - 1]["rank"].clone())?,
-        )
-    } else {
-        (0.0, 0.0)
-    };
-
-    for task in tasks {
-        let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
-        let state = match events.last() {
-            Some(s) => s["action"].as_str().unwrap(),
-            None => "open",
-        };
-
-        let rank = task["rank"].as_f64().unwrap_or(0.0) as f32;
-
-        let (max_style, min_style, mid_style, gen_style) = if state == "open" {
-            ("bFC", "Fb", "Fc", "")
-        } else {
-            ("iFYBd", "iFYBd", "iFYBd", "iFYBd")
-        };
-
-        table.add_row(Row::new(vec![
-            Cell::new(&task["id"].to_string()).style_spec(gen_style),
-            Cell::new(task["title"].as_str().unwrap()).style_spec(gen_style),
-            Cell::new(&get_from_task(task.clone(), "project")?).style_spec(gen_style),
-            Cell::new(&get_from_task(task.clone(), "assign")?).style_spec(gen_style),
-            Cell::new(&timestamp_to_date(task["due"].clone(), "date")).style_spec(gen_style),
-            if rank == max_rank {
-                Cell::new(&rank.to_string()).style_spec(max_style)
-            } else if rank == min_rank {
-                Cell::new(&rank.to_string()).style_spec(min_style)
-            } else {
-                Cell::new(&rank.to_string()).style_spec(mid_style)
-            },
-        ]));
-    }
-    table.printstd();
-
-    Ok(())
-}

+ 125 - 0
bin/tau/tau-cli/src/view.rs

@@ -0,0 +1,125 @@
+use prettytable::{cell, format, row, table, Cell, Row, Table};
+
+use darkfi::Result;
+
+use super::{
+    filter::apply_filter,
+    primitives::{Comment, TaskEvent, TaskInfo},
+    util::timestamp_to_date,
+};
+
+pub fn print_list_of_task(tasks: &mut Vec<TaskInfo>, filters: Vec<String>) -> Result<()> {
+    let mut table = Table::new();
+
+    table.set_format(
+        format::FormatBuilder::new()
+            .padding(1, 1)
+            .separators(
+                &[format::LinePosition::Title],
+                format::LineSeparator::new('─', ' ', ' ', ' '),
+            )
+            .build(),
+    );
+
+    table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
+
+    for filter in filters {
+        apply_filter(tasks, filter);
+    }
+
+    tasks.sort_by(|a, b| b.rank.partial_cmp(&a.rank).unwrap());
+
+    let mut min_rank = 0.0;
+    let mut max_rank = 0.0;
+    if tasks.first().is_some() {
+        max_rank = tasks.first().unwrap().rank;
+    }
+    if tasks.last().is_some() {
+        min_rank = tasks.last().unwrap().rank;
+    }
+
+    for task in tasks {
+        let state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
+
+        let (max_style, min_style, mid_style, gen_style) = if state == "open" {
+            ("bFC", "Fb", "Fc", "")
+        } else {
+            ("iFYBd", "iFYBd", "iFYBd", "iFYBd")
+        };
+
+        let rank = task.rank.to_string();
+
+        table.add_row(Row::new(vec![
+            Cell::new(&task.id.to_string()).style_spec(gen_style),
+            Cell::new(&task.title).style_spec(gen_style),
+            Cell::new(&task.project.join(", ")).style_spec(gen_style),
+            Cell::new(&task.assign.join(", ")).style_spec(gen_style),
+            Cell::new(&timestamp_to_date(task.due.unwrap_or(0), "date")).style_spec(gen_style),
+            if task.rank == max_rank {
+                Cell::new(&rank).style_spec(max_style)
+            } else if task.rank == min_rank {
+                Cell::new(&rank).style_spec(min_style)
+            } else {
+                Cell::new(&rank).style_spec(mid_style)
+            },
+        ]));
+    }
+    table.printstd();
+
+    Ok(())
+}
+
+pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> {
+    let current_state = &taskinfo.events.last().unwrap_or(&TaskEvent::default()).action.clone();
+    let due = timestamp_to_date(taskinfo.due.unwrap_or(0), "date");
+    let created_at = timestamp_to_date(taskinfo.created_at, "datetime");
+    let mut table = table!([Bd => "ref_id", &taskinfo.ref_id],
+                           ["id", &taskinfo.id.to_string()],
+                           [Bd =>"owner", &taskinfo.owner],
+                           [Bd =>"title", &taskinfo.title],
+                           ["desc", &taskinfo.desc.to_string()],
+                           [Bd =>"assign", taskinfo.assign.join(", ")],
+                           ["project", taskinfo.project.join(", ")],
+                           [Bd =>"due", due],
+                           ["rank", &taskinfo.rank.to_string()],
+                           [Bd =>"created_at", created_at],
+                           ["current_state", current_state]);
+
+    table.set_format(
+        format::FormatBuilder::new()
+            .padding(1, 1)
+            .separators(
+                &[format::LinePosition::Title],
+                format::LineSeparator::new('─', ' ', ' ', ' '),
+            )
+            .build(),
+    );
+    table.set_titles(row!["Name", "Value"]);
+
+    table.printstd();
+
+    let mut event_table = table!(["events", &events_as_string(taskinfo.events)]);
+    event_table.set_format(*format::consts::FORMAT_NO_COLSEP);
+
+    event_table.printstd();
+
+    Ok(())
+}
+
+pub fn comments_as_string(comments: Vec<Comment>) -> String {
+    let mut comments_str = String::new();
+    for comment in comments {
+        comments_str.push_str(&comment.to_string());
+        comments_str.push('\n');
+    }
+    comments_str
+}
+
+pub fn events_as_string(events: Vec<TaskEvent>) -> String {
+    let mut events_str = String::new();
+    for event in events {
+        events_str.push_str(&event.to_string());
+        events_str.push('\n');
+    }
+    events_str
+}

+ 60 - 57
bin/tau/taud/src/jsonrpc.rs

@@ -34,7 +34,7 @@ struct BaseTaskInfo {
     assign: Vec<String>,
     assign: Vec<String>,
     project: Vec<String>,
     project: Vec<String>,
     due: Option<Timestamp>,
     due: Option<Timestamp>,
-    rank: f32,
+    rank: Option<f32>,
 }
 }
 
 
 #[async_trait]
 #[async_trait]
@@ -54,10 +54,9 @@ impl RequestHandler for JsonRpcInterface {
             Some("add") => self.add(req.params).await,
             Some("add") => self.add(req.params).await,
             Some("list") => self.list(req.params).await,
             Some("list") => self.list(req.params).await,
             Some("update") => self.update(req.params).await,
             Some("update") => self.update(req.params).await,
-            Some("get_state") => self.get_state(req.params).await,
             Some("set_state") => self.set_state(req.params).await,
             Some("set_state") => self.set_state(req.params).await,
             Some("set_comment") => self.set_comment(req.params).await,
             Some("set_comment") => self.set_comment(req.params).await,
-            Some("get_by_id") => self.get_by_id(req.params).await,
+            Some("get_task_by_id") => self.get_task_by_id(req.params).await,
             Some(_) | None => {
             Some(_) | None => {
                 return JsonResult::Err(jsonerr(ErrorCode::MethodNotFound, None, req.id))
                 return JsonResult::Err(jsonerr(ErrorCode::MethodNotFound, None, req.id))
             }
             }
@@ -101,7 +100,7 @@ impl JsonRpcInterface {
             &task.desc,
             &task.desc,
             &self.nickname,
             &self.nickname,
             task.due,
             task.due,
-            task.rank,
+            task.rank.unwrap_or(0.0),
             &self.dataset_path,
             &self.dataset_path,
         )?;
         )?;
         new_task.set_project(&task.project);
         new_task.set_project(&task.project);
@@ -141,23 +140,6 @@ impl JsonRpcInterface {
         Ok(json!(true))
         Ok(json!(true))
     }
     }
 
 
-    // RPCAPI:
-    // Get task's state.
-    // --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
-    async fn get_state(&self, params: Value) -> TaudResult<Value> {
-        debug!(target: "tau", "JsonRpc::get_state() params {}", params);
-        let args = params.as_array().unwrap();
-
-        if args.len() != 1 {
-            return Err(TaudError::InvalidData("len of params should be 1".into()))
-        }
-
-        let task: TaskInfo = self.load_task_by_id(&args[0])?;
-
-        Ok(json!(task.get_state()))
-    }
-
     // RPCAPI:
     // RPCAPI:
     // Set state for a task and returns `true` upon success.
     // Set state for a task and returns `true` upon success.
     // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
     // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
@@ -203,10 +185,10 @@ impl JsonRpcInterface {
 
 
     // RPCAPI:
     // RPCAPI:
     // Get a task by id.
     // Get a task by id.
-    // --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1}
+    // --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
     // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
-    async fn get_by_id(&self, params: Value) -> TaudResult<Value> {
-        debug!(target: "tau", "JsonRpc::get_by_id() params {}", params);
+    async fn get_task_by_id(&self, params: Value) -> TaudResult<Value> {
+        debug!(target: "tau", "JsonRpc::get_task_by_id() params {}", params);
         let args = params.as_array().unwrap();
         let args = params.as_array().unwrap();
 
 
         if args.len() != 1 {
         if args.len() != 1 {
@@ -227,51 +209,72 @@ impl JsonRpcInterface {
         task.ok_or(TaudError::InvalidId)
         task.ok_or(TaudError::InvalidId)
     }
     }
 
 
-    fn check_params_for_update(&self, task_id: &Value, data: &Value) -> TaudResult<TaskInfo> {
+    fn check_params_for_update(&self, task_id: &Value, fields: &Value) -> TaudResult<TaskInfo> {
         let mut task: TaskInfo = self.load_task_by_id(task_id)?;
         let mut task: TaskInfo = self.load_task_by_id(task_id)?;
 
 
-        if !data.is_object() {
+        if !fields.is_array() {
             return Err(TaudError::InvalidData("Invalid task's data".into()))
             return Err(TaudError::InvalidData("Invalid task's data".into()))
         }
         }
 
 
-        let data = data.as_object().unwrap();
+        let fields = fields.as_array().unwrap();
 
 
-        if data.contains_key("title") {
-            let title = data.get("title").unwrap().clone();
-            let title: String = serde_json::from_value(title)?;
-            task.set_title(&title);
-        }
+        for field in fields {
+            if !field.is_object() {
+                return Err(TaudError::InvalidData("Invalid task's fields".into()))
+            }
 
 
-        if data.contains_key("description") {
-            let description = data.get("description").unwrap().clone();
-            let description: String = serde_json::from_value(description)?;
-            task.set_desc(&description);
-        }
+            let field = field.as_object().unwrap();
 
 
-        if data.contains_key("rank") {
-            let rank = data.get("rank").unwrap().clone();
-            let rank: f32 = serde_json::from_value(rank)?;
-            task.set_rank(rank);
-        }
+            if field.contains_key("title") {
+                let title = field.get("title").unwrap().clone();
+                let title: String = serde_json::from_value(title)?;
+                if !title.is_empty() {
+                    task.set_title(&title);
+                }
+            }
 
 
-        if data.contains_key("due") {
-            let due = data.get("due").unwrap().clone();
-            let due = serde_json::from_value(due)?;
-            task.set_due(Some(due));
-        }
+            if field.contains_key("desc") {
+                let description = field.get("description");
+                if let Some(description) = description {
+                    let description: String = serde_json::from_value(description.clone())?;
+                    task.set_desc(&description);
+                }
+            }
 
 
-        if data.contains_key("assign") {
-            let assign = data.get("assign").unwrap().clone();
-            let assign: Vec<String> = serde_json::from_value(assign)?;
-            task.set_assign(&assign);
-        }
+            if field.contains_key("rank") {
+                let rank_opt = field.get("rank");
+                if let Some(rank) = rank_opt {
+                    let rank: Option<f32> = serde_json::from_value(rank.clone())?;
+                    if rank.is_some() {
+                        task.set_rank(rank.unwrap());
+                    }
+                }
+            }
 
 
-        if data.contains_key("project") {
-            let project = data.get("project").unwrap().clone();
-            let project: Vec<String> = serde_json::from_value(project)?;
-            task.set_project(&project);
-        }
+            if field.contains_key("due") {
+                let due = field.get("due").unwrap().clone();
+                let due: Option<Option<Timestamp>> = serde_json::from_value(due)?;
+                if let Some(d) = due {
+                    task.set_due(d);
+                }
+            }
+
+            if field.contains_key("assign") {
+                let assign = field.get("assign").unwrap().clone();
+                let assign: Vec<String> = serde_json::from_value(assign)?;
+                if !assign.is_empty() {
+                    task.set_assign(&assign);
+                }
+            }
 
 
+            if field.contains_key("project") {
+                let project = field.get("project").unwrap().clone();
+                let project: Vec<String> = serde_json::from_value(project)?;
+                if !project.is_empty() {
+                    task.set_project(&project);
+                }
+            }
+        }
         Ok(task)
         Ok(task)
     }
     }
 }
 }