From 84adbdc0494df4214243f3abdd61082d9a4de5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominykas=20Dja=C4=8Denko?= Date: Mon, 16 Jan 2023 11:47:45 -0800 Subject: [PATCH] Enable support for APIv2 (#83) * Make the project work with v2 of the REST API * Get rid of all warnings * Make color strings * Make project work again with real data * Go through the whole migration plan in source * Make all the tests pass * Enforce sorting of list --- src/api/color.rs | 43 -- src/api/deserialize.rs | 25 -- src/api/mod.rs | 4 - src/api/rest/comment.rs | 8 +- src/api/rest/display.rs | 2 +- src/api/rest/gateway.rs | 268 +++++++------ src/api/rest/label.rs | 25 +- src/api/rest/mod.rs | 2 +- src/api/rest/project.rs | 71 ++-- src/api/rest/section.rs | 10 +- src/api/rest/task.rs | 57 +-- src/api/sync/data.rs | 7 +- src/api/sync/state.rs | 4 +- src/api/tree.rs | 95 ++--- src/config.rs | 2 +- src/interactive.rs | 18 +- src/labels/delete.rs | 2 +- src/labels/label.rs | 2 +- src/projects/comment.rs | 2 +- src/projects/delete.rs | 2 +- src/projects/list.rs | 24 +- src/projects/state.rs | 8 +- src/projects/view.rs | 6 +- src/sections/add.rs | 2 +- src/sections/delete.rs | 2 +- src/tasks/add.rs | 8 +- src/tasks/close.rs | 12 +- src/tasks/edit.rs | 2 +- src/tasks/filter.rs | 6 +- src/tasks/list.rs | 18 +- src/tasks/state.rs | 21 +- src/tasks/view.rs | 4 +- tests/commands/fixtures/labels.json | 12 +- tests/commands/fixtures/projects.json | 92 +++-- tests/commands/fixtures/sections.json | 32 +- tests/commands/fixtures/tasks.json | 433 ++++++++++----------- tests/commands/fixtures/tasks_partial.json | 41 +- tests/commands/mocks.rs | 10 +- 38 files changed, 685 insertions(+), 697 deletions(-) delete mode 100644 src/api/color.rs delete mode 100644 src/api/deserialize.rs diff --git a/src/api/color.rs b/src/api/color.rs deleted file mode 100644 index ed71d61..0000000 --- a/src/api/color.rs +++ /dev/null @@ -1,43 +0,0 @@ -/// Maps the color number used by the Todoist API to a specific color name. -#[allow(missing_docs)] -#[derive( - Clone, - Copy, - Debug, - serde_repr::Serialize_repr, - serde_repr::Deserialize_repr, - PartialEq, - Eq, - Ord, - PartialOrd, -)] -#[repr(u16)] -pub enum Color { - Unknown, - BerryRed = 30, - Red, - Orange, - Yellow, - OliveGreen, - LimeGreen, - Green, - MintGreen, - Teal, - SkyBlue, - LightBlue, - Blue, - Grape, - Violet, - Lavender, - Magenta, - Salmon, - Charcoal, - Grey, - Taupe, -} - -impl Default for Color { - fn default() -> Self { - Color::Unknown - } -} diff --git a/src/api/deserialize.rs b/src/api/deserialize.rs deleted file mode 100644 index 1e74894..0000000 --- a/src/api/deserialize.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Deserializer}; - -/// When deserializing and the content is a zero, it returns [`Option::None`] otherwise -/// [`Option::Some`] with the value that was deserialized. -pub(crate) fn deserialize_zero_to_none<'de, D, T: Deserialize<'de> + num_traits::Zero>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - struct Value(Option); - let v: Value = Deserialize::deserialize(deserializer)?; - let result = match v.0 { - Some(v) => { - if v.is_zero() { - None - } else { - Some(v) - } - } - None => None, - }; - Ok(result) -} diff --git a/src/api/mod.rs b/src/api/mod.rs index b57bd95..62e70a4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,9 +2,5 @@ pub mod rest; pub mod tree; -mod color; -mod deserialize; mod serialize; mod sync; - -pub use color::*; diff --git a/src/api/rest/comment.rs b/src/api/rest/comment.rs index e2925ec..e149b12 100644 --- a/src/api/rest/comment.rs +++ b/src/api/rest/comment.rs @@ -5,10 +5,10 @@ use crate::api::serialize::todoist_rfc3339; use super::{ProjectID, TaskID}; /// CommentID describes the unique ID of a [`Comment`]. -pub type CommentID = u64; +pub type CommentID = String; /// ThreadID is the ID of the location where the comment is posted. -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum ThreadID { /// The ID of the project this comment is attached to. @@ -25,7 +25,7 @@ pub enum ThreadID { /// Comment describes a Comment from the Todoist API. /// -/// Taken from the [Developer Documentation](https://developer.todoist.com/rest/v1/#comments) +/// Taken from the [Developer Documentation](https://developer.todoist.com/rest/v2/#comments) #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Comment { /// The unique ID of a comment. @@ -35,7 +35,7 @@ pub struct Comment { pub thread: ThreadID, /// The date when the comment was posted. #[serde(serialize_with = "todoist_rfc3339")] - pub posted: chrono::DateTime, + pub posted_at: chrono::DateTime, /// Contains the comment text with markdown. pub content: String, /// Optional attachment file description. diff --git a/src/api/rest/display.rs b/src/api/rest/display.rs index be55ee3..9a0732c 100644 --- a/src/api/rest/display.rs +++ b/src/api/rest/display.rs @@ -17,7 +17,7 @@ impl std::fmt::Display for FullComment<'_> { .id .if_supports_color(Stream::Stdout, |text| text.bright_yellow()) )?; - writeln!(f, "Posted: {}", comment.posted)?; + writeln!(f, "Posted: {}", comment.posted_at)?; writeln!( f, "Attachment: {}", diff --git a/src/api/rest/gateway.rs b/src/api/rest/gateway.rs index 3346343..adc7d2a 100644 --- a/src/api/rest/gateway.rs +++ b/src/api/rest/gateway.rs @@ -51,8 +51,8 @@ impl Gateway { /// Retuns a [`Task`]. /// /// * `id` - the ID as used by the Todoist API. - pub async fn task(&self, id: TaskID) -> Result { - self.get::<(), _>(&format!("rest/v1/tasks/{}", id), None) + pub async fn task(&self, id: &TaskID) -> Result { + self.get::<(), _>(&format!("rest/v2/tasks/{}", id), None) .await .wrap_err("unable to get task") } @@ -62,7 +62,7 @@ impl Gateway { /// * `filter` - a filter query as described in the [documentation](https://todoist.com/help/articles/205248842). pub async fn tasks(&self, filter: Option<&str>) -> Result> { self.get( - "rest/v1/tasks", + "rest/v2/tasks", filter.map(|filter| vec![("filter", filter)]), ) .await @@ -72,9 +72,9 @@ impl Gateway { /// Closes a task. /// /// Equivalent to pushing the circle in the UI. - pub async fn close(&self, id: TaskID) -> Result<()> { + pub async fn close(&self, id: &TaskID) -> Result<()> { self.post_empty( - &format!("rest/v1/tasks/{}/close", id), + &format!("rest/v2/tasks/{}/close", id), &serde_json::Map::new(), ) .await @@ -86,7 +86,7 @@ impl Gateway { /// recurring, it will stop doing that. /// This is a bit hacky, but the REST API does not support completely closing tasks without /// deleting them. - pub async fn complete(&self, id: TaskID) -> Result<()> { + pub async fn complete(&self, id: &TaskID) -> Result<()> { self.update( id, &UpdateTask { @@ -102,15 +102,15 @@ impl Gateway { /// Creates a task by calling the Todoist API. pub async fn create(&self, task: &CreateTask) -> Result { - self.post("rest/v1/tasks", task) + self.post("rest/v2/tasks", task) .await .wrap_err("unable to create task")? .ok_or_else(|| eyre!("unable to create task")) } /// Updates a task with the data as specified in UpdateTask. - pub async fn update(&self, id: TaskID, task: &UpdateTask) -> Result<()> { - self.post_empty(&format!("rest/v1/tasks/{}", id), &task) + pub async fn update(&self, id: &TaskID, task: &UpdateTask) -> Result<()> { + self.post_empty(&format!("rest/v2/tasks/{}", id), &task) .await .wrap_err("unable to update task")?; Ok(()) @@ -118,42 +118,42 @@ impl Gateway { /// Returns the list of Projects. pub async fn projects(&self) -> Result> { - self.get::<(), _>("rest/v1/projects", None) + self.get::<(), _>("rest/v2/projects", None) .await .wrap_err("unable to get projects") } /// Returns the list of all Sections. pub async fn sections(&self) -> Result> { - self.get::<(), _>("rest/v1/sections", None) + self.get::<(), _>("rest/v2/sections", None) .await .wrap_err("unable to get sections") } /// Returns the list of all Labels. pub async fn labels(&self) -> Result> { - self.get::<(), _>("rest/v1/labels", None) + self.get::<(), _>("rest/v2/labels", None) .await .wrap_err("unable to get labels") } /// Returns the list of all comments attached to the given Project. - pub async fn project_comments(&self, id: ProjectID) -> Result> { - self.get("rest/v1/comments", Some(&[("project_id", id)])) + pub async fn project_comments(&self, id: &ProjectID) -> Result> { + self.get("rest/v2/comments", Some(&[("project_id", id)])) .await .wrap_err("unable to get comments") } /// Returns the list of all comments attached to the given Task. - pub async fn task_comments(&self, id: TaskID) -> Result> { - self.get("rest/v1/comments", Some(&[("task_id", id)])) + pub async fn task_comments(&self, id: &TaskID) -> Result> { + self.get("rest/v2/comments", Some(&[("task_id", id)])) .await .wrap_err("unable to get comments") } /// Creates a comment by calling the API. pub async fn create_comment(&self, comment: &CreateComment) -> Result { - self.post("rest/v1/comments", comment) + self.post("rest/v2/comments", comment) .await .wrap_err("unable to create comment")? .ok_or_else(|| eyre!("unable to create comment")) @@ -162,23 +162,23 @@ impl Gateway { /// Returns details about a single project. /// /// * `id` - the ID as used by the Todoist API. - pub async fn project(&self, id: ProjectID) -> Result { - self.get::<(), _>(&format!("rest/v1/projects/{}", id), None) + pub async fn project(&self, id: &ProjectID) -> Result { + self.get::<(), _>(&format!("rest/v2/projects/{}", id), None) .await .wrap_err("unable to get project") } /// Creates a project by calling the Todoist API. pub async fn create_project(&self, project: &CreateProject) -> Result { - self.post("rest/v1/projects", project) + self.post("rest/v2/projects", project) .await .wrap_err("unable to create project")? .ok_or_else(|| eyre!("unable to create project")) } /// Deletes a project by calling the Todoist API. - pub async fn delete_project(&self, project: ProjectID) -> Result<()> { - self.delete(&format!("rest/v1/projects/{}", project)) + pub async fn delete_project(&self, project: &ProjectID) -> Result<()> { + self.delete(&format!("rest/v2/projects/{}", project)) .await .wrap_err("unable to delete project") } @@ -186,23 +186,23 @@ impl Gateway { /// Returns details about a single section. /// /// * `id` - the ID as used by the Todoist API. - pub async fn section(&self, id: SectionID) -> Result
{ - self.get::<(), _>(&format!("rest/v1/sections/{}", id), None) + pub async fn section(&self, id: &SectionID) -> Result
{ + self.get::<(), _>(&format!("rest/v2/sections/{}", id), None) .await .wrap_err("unable to get section") } /// Creates a section by calling the Todoist API. pub async fn create_section(&self, section: &CreateSection) -> Result
{ - self.post("rest/v1/sections", section) + self.post("rest/v2/sections", section) .await .wrap_err("unable to create section")? .ok_or_else(|| eyre!("unable to create section")) } /// Deletes a section by calling the Todoist API. - pub async fn delete_section(&self, section: SectionID) -> Result<()> { - self.delete(&format!("rest/v1/sections/{}", section)) + pub async fn delete_section(&self, section: &SectionID) -> Result<()> { + self.delete(&format!("rest/v2/sections/{}", section)) .await .wrap_err("unable to delete section") } @@ -210,23 +210,23 @@ impl Gateway { /// Returns details about a single label. /// /// * `id` - the ID as used by the Todoist API. - pub async fn label(&self, id: LabelID) -> Result