From 9d63911fc176a2b8baa042112b12ccd42429629d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:45:29 -0500 Subject: [PATCH] feat: add lists endpoint with save and delete list item methods Implement SaveListItemRequest and SaveListItemResponse models. Add save_list_item (create/update via PUT) and delete_list_item methods. Tests cover creating, updating, deleting list items and not-found error. --- src/endpoints/lists.rs | 25 ++++++++++++ src/endpoints/mod.rs | 1 + src/models/list.rs | 22 +++++++++++ src/models/mod.rs | 2 + tests/test_lists.rs | 86 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 src/endpoints/lists.rs create mode 100644 src/models/list.rs create mode 100644 tests/test_lists.rs diff --git a/src/endpoints/lists.rs b/src/endpoints/lists.rs new file mode 100644 index 0000000..46fd732 --- /dev/null +++ b/src/endpoints/lists.rs @@ -0,0 +1,25 @@ +use reqwest::Method; +use uuid::Uuid; + +use crate::client::OnspringClient; +use crate::error::Result; +use crate::models::{SaveListItemRequest, SaveListItemResponse}; + +impl OnspringClient { + /// Creates or updates a list item in the specified list. + pub async fn save_list_item( + &self, + list_id: i32, + request: SaveListItemRequest, + ) -> Result { + let path = format!("/Lists/id/{}/items", list_id); + self.request(Method::PUT, &path, &[], Some(&request)).await + } + + /// Deletes a list item from the specified list. + pub async fn delete_list_item(&self, list_id: i32, item_id: Uuid) -> Result<()> { + let path = format!("/Lists/id/{}/itemId/{}", list_id, item_id); + self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) + .await + } +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 8aea80c..a5a260b 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,5 +1,6 @@ mod apps; mod fields; mod files; +mod lists; mod ping; mod records; diff --git a/src/models/list.rs b/src/models/list.rs new file mode 100644 index 0000000..1b7fec2 --- /dev/null +++ b/src/models/list.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Request to create or update a list item. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveListItemRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub numeric_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, +} + +/// Response from saving a list item. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveListItemResponse { + pub id: Uuid, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index a85045a..9379529 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,6 +2,7 @@ mod app; mod enums; mod field; pub mod file; +pub mod list; mod paging; pub mod record; @@ -9,5 +10,6 @@ pub use app::*; pub use enums::*; pub use field::*; pub use file::*; +pub use list::*; pub use paging::*; pub use record::*; diff --git a/tests/test_lists.rs b/tests/test_lists.rs new file mode 100644 index 0000000..1c42bb9 --- /dev/null +++ b/tests/test_lists.rs @@ -0,0 +1,86 @@ +mod common; + +use uuid::Uuid; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +#[tokio::test] +async fn test_save_list_item_create() { + let (mock_server, client) = common::setup().await; + let new_id = Uuid::new_v4(); + + Mock::given(method("PUT")) + .and(path("/Lists/id/10/items")) + .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ + "id": new_id.to_string() + }))) + .mount(&mock_server) + .await; + + let request = onspring::models::list::SaveListItemRequest { + id: None, + name: "New Item".to_string(), + numeric_value: Some(1.0), + color: Some("#ff0000".to_string()), + }; + let result = client.save_list_item(10, request).await.unwrap(); + assert_eq!(result.id, new_id); +} + +#[tokio::test] +async fn test_save_list_item_update() { + let (mock_server, client) = common::setup().await; + let existing_id = Uuid::new_v4(); + + Mock::given(method("PUT")) + .and(path("/Lists/id/10/items")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": existing_id.to_string() + }))) + .mount(&mock_server) + .await; + + let request = onspring::models::list::SaveListItemRequest { + id: Some(existing_id), + name: "Updated Item".to_string(), + numeric_value: None, + color: None, + }; + let result = client.save_list_item(10, request).await.unwrap(); + assert_eq!(result.id, existing_id); +} + +#[tokio::test] +async fn test_delete_list_item() { + let (mock_server, client) = common::setup().await; + let item_id = Uuid::new_v4(); + + Mock::given(method("DELETE")) + .and(path(format!("/Lists/id/10/itemId/{}", item_id))) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; + + let result = client.delete_list_item(10, item_id).await; + assert!(result.is_ok()); +} + +#[tokio::test] +async fn test_delete_list_item_not_found() { + let (mock_server, client) = common::setup().await; + let item_id = Uuid::new_v4(); + + Mock::given(method("DELETE")) + .and(path(format!("/Lists/id/10/itemId/{}", item_id))) + .respond_with(ResponseTemplate::new(404)) + .mount(&mock_server) + .await; + + let result = client.delete_list_item(10, item_id).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 404); + } else { + panic!("Expected Api error"); + } +}