diff --git a/README.md b/README.md index 6ec0cde..19d6f08 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ use onspring::OnspringClient; use std::env; let client = OnspringClient::builder(env::var("API_KEY").unwrap()) - .base_url(env::var("BASE_URL").unwrap()) - .build(); + .base_url(env::var("BASE_URL").unwrap()) + .build(); ``` ### Client Configuration @@ -99,18 +99,18 @@ use std::time::Duration; // Custom timeout let client = OnspringClient::builder("your-api-key") - .timeout(Duration::from_secs(30)) - .build(); + .timeout(Duration::from_secs(30)) + .build(); // Custom reqwest client let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(60)) - .build() - .unwrap(); + .timeout(Duration::from_secs(60)) + .build() + .unwrap(); let client = OnspringClient::builder("your-api-key") - .http_client(http_client) - .build(); + .http_client(http_client) + .build(); ``` ### Error Handling @@ -124,11 +124,11 @@ All client methods return `onspring::Result`, which is an alias for `Result println!("App: {}", app.name.unwrap_or_default()), - Err(onspring::OnspringError::Api { status_code, message }) => { - eprintln!("API error {}: {}", status_code, message); - } - Err(e) => eprintln!("Error: {}", e), + Ok(app) => println!("App: {}", app.name.unwrap_or_default()), + Err(onspring::OnspringError::Api { status_code, message }) => { + eprintln!("API error {}: {}", status_code, message); + } + Err(e) => eprintln!("Error: {}", e), } ``` @@ -160,7 +160,7 @@ let res = client.list_apps(None).await?; let apps = res.items.unwrap_or_default(); for app in &apps { - println!("{:?}", app); + println!("{:?}", app); } ``` @@ -174,7 +174,7 @@ let res = client.list_apps(Some(paging)).await?; let apps = res.items.unwrap_or_default(); for app in &apps { - println!("{:?}", app); + println!("{:?}", app); } ``` @@ -196,7 +196,7 @@ let res = client.batch_get_apps(&[130, 131]).await?; let apps = res.items.unwrap_or_default(); for app in &apps { - println!("{:?}", app); + println!("{:?}", app); } ``` @@ -220,7 +220,7 @@ let res = client.batch_get_fields(&[4793, 4801]).await?; let fields = res.items.unwrap_or_default(); for field in &fields { - println!("{:?}", field); + println!("{:?}", field); } ``` @@ -233,7 +233,7 @@ let res = client.list_fields(132, None).await?; let fields = res.items.unwrap_or_default(); for field in &fields { - println!("{:?}", field); + println!("{:?}", field); } ``` @@ -247,7 +247,7 @@ let res = client.list_fields(132, Some(paging)).await?; let fields = res.items.unwrap_or_default(); for field in &fields { - println!("{:?}", field); + println!("{:?}", field); } ``` @@ -275,7 +275,7 @@ println!("Content-Type: {:?}", file.content_type); println!("File Name: {:?}", file.file_name); if let Some(name) = &file.file_name { - fs::write(name, &file.data)?; + fs::write(name, &file.data)?; } ``` @@ -285,13 +285,13 @@ if let Some(name) = &file.file_name { use onspring::models::file::SaveFileRequest; let request = SaveFileRequest { - record_id: 1, - field_id: 4806, - notes: Some("notes".to_string()), - modified_date: None, - file_name: "test-attachment.txt".to_string(), - file_data: std::fs::read("test-attachment.txt")?, - content_type: "text/plain".to_string(), + record_id: 1, + field_id: 4806, + notes: Some("notes".to_string()), + modified_date: None, + file_name: "test-attachment.txt".to_string(), + file_data: std::fs::read("test-attachment.txt")?, + content_type: "text/plain".to_string(), }; let res = client.upload_file(request).await?; @@ -315,10 +315,10 @@ To add a list value don't provide an id value. use onspring::models::list::SaveListItemRequest; let request = SaveListItemRequest { - id: None, - name: "New Value".to_string(), - numeric_value: Some(1.0), - color: Some("#000000".to_string()), + id: None, + name: "New Value".to_string(), + numeric_value: Some(1.0), + color: Some("#000000".to_string()), }; let res = client.save_list_item(638, request).await?; @@ -334,10 +334,10 @@ use uuid::Uuid; let item_id: Uuid = "35c79a46-04b8-4069-bbc1-161a175f962c".parse().unwrap(); let request = SaveListItemRequest { - id: Some(item_id), - name: "Updated Value".to_string(), - numeric_value: Some(1.0), - color: Some("#000000".to_string()), + id: Some(item_id), + name: "Updated Value".to_string(), + numeric_value: Some(1.0), + color: Some("#000000".to_string()), }; let res = client.save_list_item(638, request).await?; @@ -365,7 +365,7 @@ let res = client.list_records(130, None, None, None).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -376,15 +376,15 @@ use onspring::{DataFormat, PagingRequest}; let paging = PagingRequest { page_number: 1, page_size: 10 }; let res = client.list_records( - 130, - Some(paging), - Some(&[4804]), - Some(DataFormat::Raw), + 130, + Some(paging), + Some(&[4804]), + Some(DataFormat::Raw), ).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -414,17 +414,17 @@ Returns a collection of Onspring records based on the provided app id and record use onspring::models::record::BatchGetRecordsRequest; let request = BatchGetRecordsRequest { - app_id: 130, - record_ids: vec![1, 2], - field_ids: None, - data_format: None, + app_id: 130, + record_ids: vec![1, 2], + field_ids: None, + data_format: None, }; let res = client.batch_get_records(request).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -434,17 +434,17 @@ You can also specify what field values to return and in what format (Raw vs. For use onspring::{DataFormat, models::record::BatchGetRecordsRequest}; let request = BatchGetRecordsRequest { - app_id: 130, - record_ids: vec![1, 2], - field_ids: Some(vec![4804]), - data_format: Some(DataFormat::Formatted), + app_id: 130, + record_ids: vec![1, 2], + field_ids: Some(vec![4804]), + data_format: Some(DataFormat::Formatted), }; let res = client.batch_get_records(request).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -456,17 +456,17 @@ Returns a paged collection of records based on a criteria that can be paged thro use onspring::models::record::QueryRecordsRequest; let request = QueryRecordsRequest { - app_id: 130, - filter: "not (4745 eq 0)".to_string(), - field_ids: None, - data_format: None, + app_id: 130, + filter: "not (4745 eq 0)".to_string(), + field_ids: None, + data_format: None, }; let res = client.query_records(request, None).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -476,10 +476,10 @@ You can set your own page size and page number (max is 1,000) as well. In additi use onspring::{DataFormat, PagingRequest, models::record::QueryRecordsRequest}; let request = QueryRecordsRequest { - app_id: 130, - filter: "not (4745 eq 0)".to_string(), - field_ids: Some(vec![4804]), - data_format: Some(DataFormat::Formatted), + app_id: 130, + filter: "not (4745 eq 0)".to_string(), + field_ids: Some(vec![4804]), + data_format: Some(DataFormat::Formatted), }; let paging = PagingRequest { page_number: 1, page_size: 10 }; @@ -487,7 +487,7 @@ let res = client.query_records(request, Some(paging)).await?; let records = res.items.unwrap_or_default(); for record in &records { - println!("{:?}", record); + println!("{:?}", record); } ``` @@ -505,9 +505,9 @@ let mut fields = HashMap::new(); fields.insert("4804".to_string(), serde_json::json!("Test")); let request = SaveRecordRequest { - app_id: 130, - record_id: None, - fields, + app_id: 130, + record_id: None, + fields, }; let res = client.save_record(request).await?; @@ -524,9 +524,9 @@ let mut fields = HashMap::new(); fields.insert("4804".to_string(), serde_json::json!("Updated")); let request = SaveRecordRequest { - app_id: 130, - record_id: Some(607), - fields, + app_id: 130, + record_id: Some(607), + fields, }; let res = client.save_record(request).await?; @@ -550,8 +550,8 @@ Delete a batch of records based upon their ids. use onspring::models::record::BatchDeleteRecordsRequest; let request = BatchDeleteRecordsRequest { - app_id: 130, - record_ids: vec![608, 609], + app_id: 130, + record_ids: vec![608, 609], }; client.batch_delete_records(request).await?; @@ -575,9 +575,9 @@ You can also specify the format of the data in the report as well as whether you use onspring::{DataFormat, ReportDataType}; let report = client.get_report( - 409, - Some(DataFormat::Formatted), - Some(ReportDataType::ChartData), + 409, + Some(DataFormat::Formatted), + Some(ReportDataType::ChartData), ).await?; println!("{:?}", report); ``` @@ -591,7 +591,7 @@ let res = client.list_reports(130, None).await?; let reports = res.items.unwrap_or_default(); for report in &reports { - println!("{:?}", report); + println!("{:?}", report); } ``` @@ -605,6 +605,6 @@ let res = client.list_reports(130, Some(paging)).await?; let reports = res.items.unwrap_or_default(); for report in &reports { - println!("{:?}", report); + println!("{:?}", report); } ``` diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b196eaa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 diff --git a/src/client.rs b/src/client.rs index c21eb5a..24ab60b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,221 +10,221 @@ use crate::error::{OnspringError, Result}; /// Client for interacting with the Onspring API v2. pub struct OnspringClient { - http_client: reqwest::Client, - base_url: String, - api_key: String, + http_client: reqwest::Client, + base_url: String, + api_key: String, } /// Builder for constructing an [`OnspringClient`]. pub struct OnspringClientBuilder { - base_url: String, - api_key: String, - http_client: Option, - timeout: Option, + base_url: String, + api_key: String, + http_client: Option, + timeout: Option, } impl OnspringClientBuilder { - /// Creates a new builder with the given API key. - pub fn new(api_key: impl Into) -> Self { - Self { - base_url: "https://api.onspring.com".to_string(), - api_key: api_key.into(), - http_client: None, - timeout: None, - } + /// Creates a new builder with the given API key. + pub fn new(api_key: impl Into) -> Self { + Self { + base_url: "https://api.onspring.com".to_string(), + api_key: api_key.into(), + http_client: None, + timeout: None, } + } - /// Sets the base URL for the API. - pub fn base_url(mut self, url: impl Into) -> Self { - self.base_url = url.into(); - self - } + /// Sets the base URL for the API. + pub fn base_url(mut self, url: impl Into) -> Self { + self.base_url = url.into(); + self + } - /// Sets a custom `reqwest::Client` to use for HTTP requests. - pub fn http_client(mut self, client: reqwest::Client) -> Self { - self.http_client = Some(client); - self - } + /// Sets a custom `reqwest::Client` to use for HTTP requests. + pub fn http_client(mut self, client: reqwest::Client) -> Self { + self.http_client = Some(client); + self + } - /// Sets the request timeout. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } + /// Sets the request timeout. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } - /// Builds the [`OnspringClient`]. - pub fn build(self) -> OnspringClient { - let http_client = self.http_client.unwrap_or_else(|| { - reqwest::Client::builder() - .timeout(self.timeout.unwrap_or(Duration::from_secs(120))) - .build() - .expect("failed to build HTTP client") - }); + /// Builds the [`OnspringClient`]. + pub fn build(self) -> OnspringClient { + let http_client = self.http_client.unwrap_or_else(|| { + reqwest::Client::builder() + .timeout(self.timeout.unwrap_or(Duration::from_secs(120))) + .build() + .expect("failed to build HTTP client") + }); - OnspringClient { - http_client, - base_url: self.base_url.trim_end_matches('/').to_string(), - api_key: self.api_key, - } + OnspringClient { + http_client, + base_url: self.base_url.trim_end_matches('/').to_string(), + api_key: self.api_key, } + } } impl OnspringClient { - /// Creates a new [`OnspringClientBuilder`]. - pub fn builder(api_key: impl Into) -> OnspringClientBuilder { - OnspringClientBuilder::new(api_key) + /// Creates a new [`OnspringClientBuilder`]. + pub fn builder(api_key: impl Into) -> OnspringClientBuilder { + OnspringClientBuilder::new(api_key) + } + + fn default_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + "x-apikey", + HeaderValue::from_str(&self.api_key).expect("invalid API key"), + ); + headers.insert("x-api-version", HeaderValue::from_static("2")); + headers + } + + pub(crate) async fn request( + &self, + method: Method, + path: &str, + query: &[(&str, String)], + body: Option<&impl Serialize>, + ) -> Result { + let url = format!("{}{}", self.base_url, path); + let mut req = self + .http_client + .request(method, &url) + .headers(self.default_headers()) + .query(query); + + if let Some(body) = body { + req = req.json(body); } - fn default_headers(&self) -> HeaderMap { - let mut headers = HeaderMap::new(); - headers.insert( - "x-apikey", - HeaderValue::from_str(&self.api_key).expect("invalid API key"), - ); - headers.insert("x-api-version", HeaderValue::from_static("2")); - headers + let response = req.send().await?; + let status = response.status(); + + if !status.is_success() { + let message = response + .json::() + .await + .ok() + .and_then(|v| v.get("message")?.as_str().map(String::from)) + .unwrap_or_default(); + return Err(OnspringError::Api { + status_code: status.as_u16(), + message, + }); } - pub(crate) async fn request( - &self, - method: Method, - path: &str, - query: &[(&str, String)], - body: Option<&impl Serialize>, - ) -> Result { - let url = format!("{}{}", self.base_url, path); - let mut req = self - .http_client - .request(method, &url) - .headers(self.default_headers()) - .query(query); + let body = response.text().await?; + serde_json::from_str(&body).map_err(OnspringError::Serialization) + } - if let Some(body) = body { - req = req.json(body); - } + pub(crate) async fn request_no_content( + &self, + method: Method, + path: &str, + query: &[(&str, String)], + body: Option<&impl Serialize>, + ) -> Result<()> { + let url = format!("{}{}", self.base_url, path); + let mut req = self + .http_client + .request(method, &url) + .headers(self.default_headers()) + .query(query); - let response = req.send().await?; - let status = response.status(); - - if !status.is_success() { - let message = response - .json::() - .await - .ok() - .and_then(|v| v.get("message")?.as_str().map(String::from)) - .unwrap_or_default(); - return Err(OnspringError::Api { - status_code: status.as_u16(), - message, - }); - } - - let body = response.text().await?; - serde_json::from_str(&body).map_err(OnspringError::Serialization) + if let Some(body) = body { + req = req.json(body); } - pub(crate) async fn request_no_content( - &self, - method: Method, - path: &str, - query: &[(&str, String)], - body: Option<&impl Serialize>, - ) -> Result<()> { - let url = format!("{}{}", self.base_url, path); - let mut req = self - .http_client - .request(method, &url) - .headers(self.default_headers()) - .query(query); + let response = req.send().await?; + let status = response.status(); - if let Some(body) = body { - req = req.json(body); - } - - let response = req.send().await?; - let status = response.status(); - - if !status.is_success() { - let message = response - .json::() - .await - .ok() - .and_then(|v| v.get("message")?.as_str().map(String::from)) - .unwrap_or_default(); - return Err(OnspringError::Api { - status_code: status.as_u16(), - message, - }); - } - - Ok(()) + if !status.is_success() { + let message = response + .json::() + .await + .ok() + .and_then(|v| v.get("message")?.as_str().map(String::from)) + .unwrap_or_default(); + return Err(OnspringError::Api { + status_code: status.as_u16(), + message, + }); } - pub(crate) async fn request_bytes( - &self, - method: Method, - path: &str, - query: &[(&str, String)], - ) -> Result<(StatusCode, HeaderMap, Bytes)> { - let url = format!("{}{}", self.base_url, path); - let response = self - .http_client - .request(method, &url) - .headers(self.default_headers()) - .query(query) - .send() - .await?; + Ok(()) + } - let status = response.status(); + pub(crate) async fn request_bytes( + &self, + method: Method, + path: &str, + query: &[(&str, String)], + ) -> Result<(StatusCode, HeaderMap, Bytes)> { + let url = format!("{}{}", self.base_url, path); + let response = self + .http_client + .request(method, &url) + .headers(self.default_headers()) + .query(query) + .send() + .await?; - if !status.is_success() { - let message = response - .json::() - .await - .ok() - .and_then(|v| v.get("message")?.as_str().map(String::from)) - .unwrap_or_default(); - return Err(OnspringError::Api { - status_code: status.as_u16(), - message, - }); - } + let status = response.status(); - let headers = response.headers().clone(); - let bytes = response.bytes().await?; - Ok((status, headers, bytes)) + if !status.is_success() { + let message = response + .json::() + .await + .ok() + .and_then(|v| v.get("message")?.as_str().map(String::from)) + .unwrap_or_default(); + return Err(OnspringError::Api { + status_code: status.as_u16(), + message, + }); } - pub(crate) async fn request_multipart( - &self, - path: &str, - form: reqwest::multipart::Form, - ) -> Result { - let url = format!("{}{}", self.base_url, path); - let response = self - .http_client - .post(&url) - .headers(self.default_headers()) - .multipart(form) - .send() - .await?; + let headers = response.headers().clone(); + let bytes = response.bytes().await?; + Ok((status, headers, bytes)) + } - let status = response.status(); + pub(crate) async fn request_multipart( + &self, + path: &str, + form: reqwest::multipart::Form, + ) -> Result { + let url = format!("{}{}", self.base_url, path); + let response = self + .http_client + .post(&url) + .headers(self.default_headers()) + .multipart(form) + .send() + .await?; - if !status.is_success() { - let message = response - .json::() - .await - .ok() - .and_then(|v| v.get("message")?.as_str().map(String::from)) - .unwrap_or_default(); - return Err(OnspringError::Api { - status_code: status.as_u16(), - message, - }); - } + let status = response.status(); - let body = response.text().await?; - serde_json::from_str(&body).map_err(OnspringError::Serialization) + if !status.is_success() { + let message = response + .json::() + .await + .ok() + .and_then(|v| v.get("message")?.as_str().map(String::from)) + .unwrap_or_default(); + return Err(OnspringError::Api { + status_code: status.as_u16(), + message, + }); } + + let body = response.text().await?; + serde_json::from_str(&body).map_err(OnspringError::Serialization) + } } diff --git a/src/endpoints/apps.rs b/src/endpoints/apps.rs index 0b59a9a..bde1a6f 100644 --- a/src/endpoints/apps.rs +++ b/src/endpoints/apps.rs @@ -5,28 +5,31 @@ use crate::error::Result; use crate::models::{App, CollectionResponse, PagedResponse, PagingRequest}; impl OnspringClient { - /// Gets all apps for the current client, with optional pagination. - pub async fn list_apps(&self, paging: Option) -> Result> { - let mut query = Vec::new(); - if let Some(p) = paging { - query.push(("PageNumber", p.page_number.to_string())); - query.push(("PageSize", p.page_size.to_string())); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, "/Apps", &query_refs, Option::<&()>::None) - .await + /// Gets all apps for the current client, with optional pagination. + pub async fn list_apps(&self, paging: Option) -> Result> { + let mut query = Vec::new(); + if let Some(p) = paging { + query.push(("PageNumber", p.page_number.to_string())); + query.push(("PageSize", p.page_size.to_string())); } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, "/Apps", &query_refs, Option::<&()>::None) + .await + } - /// Gets an app by its identifier. - pub async fn get_app(&self, app_id: i32) -> Result { - let path = format!("/Apps/id/{}", app_id); - self.request(Method::GET, &path, &[], Option::<&()>::None) - .await - } + /// Gets an app by its identifier. + pub async fn get_app(&self, app_id: i32) -> Result { + let path = format!("/Apps/id/{}", app_id); + self + .request(Method::GET, &path, &[], Option::<&()>::None) + .await + } - /// Gets up to 100 apps by their identifiers. - pub async fn batch_get_apps(&self, ids: &[i32]) -> Result> { - self.request(Method::POST, "/Apps/batch-get", &[], Some(&ids)) - .await - } + /// Gets up to 100 apps by their identifiers. + pub async fn batch_get_apps(&self, ids: &[i32]) -> Result> { + self + .request(Method::POST, "/Apps/batch-get", &[], Some(&ids)) + .await + } } diff --git a/src/endpoints/fields.rs b/src/endpoints/fields.rs index 7b66f14..d9a9844 100644 --- a/src/endpoints/fields.rs +++ b/src/endpoints/fields.rs @@ -5,33 +5,36 @@ use crate::error::Result; use crate::models::{CollectionResponse, Field, PagedResponse, PagingRequest}; impl OnspringClient { - /// Gets a field by its identifier. - pub async fn get_field(&self, field_id: i32) -> Result { - let path = format!("/Fields/id/{}", field_id); - self.request(Method::GET, &path, &[], Option::<&()>::None) - .await - } + /// Gets a field by its identifier. + pub async fn get_field(&self, field_id: i32) -> Result { + let path = format!("/Fields/id/{}", field_id); + self + .request(Method::GET, &path, &[], Option::<&()>::None) + .await + } - /// Gets up to 100 fields by their identifiers. - pub async fn batch_get_fields(&self, ids: &[i32]) -> Result> { - self.request(Method::POST, "/Fields/batch-get", &[], Some(&ids)) - .await - } + /// Gets up to 100 fields by their identifiers. + pub async fn batch_get_fields(&self, ids: &[i32]) -> Result> { + self + .request(Method::POST, "/Fields/batch-get", &[], Some(&ids)) + .await + } - /// Gets a paginated list of fields for a given application. - pub async fn list_fields( - &self, - app_id: i32, - paging: Option, - ) -> Result> { - let path = format!("/Fields/appId/{}", app_id); - let mut query = Vec::new(); - if let Some(p) = paging { - query.push(("PageNumber", p.page_number.to_string())); - query.push(("PageSize", p.page_size.to_string())); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, &path, &query_refs, Option::<&()>::None) - .await + /// Gets a paginated list of fields for a given application. + pub async fn list_fields( + &self, + app_id: i32, + paging: Option, + ) -> Result> { + let path = format!("/Fields/appId/{}", app_id); + let mut query = Vec::new(); + if let Some(p) = paging { + query.push(("PageNumber", p.page_number.to_string())); + query.push(("PageSize", p.page_size.to_string())); } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, &path, &query_refs, Option::<&()>::None) + .await + } } diff --git a/src/endpoints/files.rs b/src/endpoints/files.rs index 9253f28..356fb86 100644 --- a/src/endpoints/files.rs +++ b/src/endpoints/files.rs @@ -5,84 +5,86 @@ use crate::error::Result; use crate::models::{CreatedWithIdResponse, FileInfo, FileResponse, SaveFileRequest}; impl OnspringClient { - /// Gets a file's metadata information. - pub async fn get_file_info( - &self, - record_id: i32, - field_id: i32, - file_id: i32, - ) -> Result { - let path = format!( - "/Files/recordId/{}/fieldId/{}/fileId/{}", - record_id, field_id, file_id - ); - self.request(Method::GET, &path, &[], Option::<&()>::None) - .await + /// Gets a file's metadata information. + pub async fn get_file_info( + &self, + record_id: i32, + field_id: i32, + file_id: i32, + ) -> Result { + let path = format!( + "/Files/recordId/{}/fieldId/{}/fileId/{}", + record_id, field_id, file_id + ); + self + .request(Method::GET, &path, &[], Option::<&()>::None) + .await + } + + /// Downloads a file's content. + pub async fn get_file( + &self, + record_id: i32, + field_id: i32, + file_id: i32, + ) -> Result { + let path = format!( + "/Files/recordId/{}/fieldId/{}/fileId/{}/file", + record_id, field_id, file_id + ); + let (_status, headers, data) = self.request_bytes(Method::GET, &path, &[]).await?; + + let content_type = headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .map(String::from); + + let file_name = headers + .get("content-disposition") + .and_then(|v| v.to_str().ok()) + .and_then(|v| { + v.split("filename=") + .nth(1) + .map(|s| s.trim_matches('"').to_string()) + }); + + Ok(FileResponse { + content_type, + file_name, + data, + }) + } + + /// Uploads a file attachment. + pub async fn upload_file(&self, request: SaveFileRequest) -> Result { + let file_part = reqwest::multipart::Part::bytes(request.file_data) + .file_name(request.file_name) + .mime_str(&request.content_type) + .map_err(|e| crate::error::OnspringError::InvalidArgument(e.to_string()))?; + + let mut form = reqwest::multipart::Form::new() + .text("RecordId", request.record_id.to_string()) + .text("FieldId", request.field_id.to_string()) + .part("File", file_part); + + if let Some(notes) = request.notes { + form = form.text("Notes", notes); + } + if let Some(date) = request.modified_date { + form = form.text("ModifiedDate", date.to_rfc3339()); } - /// Downloads a file's content. - pub async fn get_file( - &self, - record_id: i32, - field_id: i32, - file_id: i32, - ) -> Result { - let path = format!( - "/Files/recordId/{}/fieldId/{}/fileId/{}/file", - record_id, field_id, file_id - ); - let (_status, headers, data) = self.request_bytes(Method::GET, &path, &[]).await?; + self.request_multipart("/Files", form).await + } - let content_type = headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .map(String::from); - - let file_name = headers - .get("content-disposition") - .and_then(|v| v.to_str().ok()) - .and_then(|v| { - v.split("filename=") - .nth(1) - .map(|s| s.trim_matches('"').to_string()) - }); - - Ok(FileResponse { - content_type, - file_name, - data, - }) - } - - /// Uploads a file attachment. - pub async fn upload_file(&self, request: SaveFileRequest) -> Result { - let file_part = reqwest::multipart::Part::bytes(request.file_data) - .file_name(request.file_name) - .mime_str(&request.content_type) - .map_err(|e| crate::error::OnspringError::InvalidArgument(e.to_string()))?; - - let mut form = reqwest::multipart::Form::new() - .text("RecordId", request.record_id.to_string()) - .text("FieldId", request.field_id.to_string()) - .part("File", file_part); - - if let Some(notes) = request.notes { - form = form.text("Notes", notes); - } - if let Some(date) = request.modified_date { - form = form.text("ModifiedDate", date.to_rfc3339()); - } - - self.request_multipart("/Files", form).await - } - - /// Deletes a file attachment. - pub async fn delete_file(&self, record_id: i32, field_id: i32, file_id: i32) -> Result<()> { - let path = format!( - "/Files/recordId/{}/fieldId/{}/fileId/{}", - record_id, field_id, file_id - ); - self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) - .await - } + /// Deletes a file attachment. + pub async fn delete_file(&self, record_id: i32, field_id: i32, file_id: i32) -> Result<()> { + let path = format!( + "/Files/recordId/{}/fieldId/{}/fileId/{}", + record_id, field_id, file_id + ); + self + .request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) + .await + } } diff --git a/src/endpoints/lists.rs b/src/endpoints/lists.rs index 46fd732..cb53395 100644 --- a/src/endpoints/lists.rs +++ b/src/endpoints/lists.rs @@ -6,20 +6,21 @@ 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 - } + /// 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 - } + /// 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/ping.rs b/src/endpoints/ping.rs index 3cc38ff..af924b7 100644 --- a/src/endpoints/ping.rs +++ b/src/endpoints/ping.rs @@ -4,9 +4,10 @@ use crate::client::OnspringClient; use crate::error::Result; impl OnspringClient { - /// Checks if the Onspring API is reachable. - pub async fn ping(&self) -> Result<()> { - self.request_no_content(Method::GET, "/Ping", &[], Option::<&()>::None) - .await - } + /// Checks if the Onspring API is reachable. + pub async fn ping(&self) -> Result<()> { + self + .request_no_content(Method::GET, "/Ping", &[], Option::<&()>::None) + .await + } } diff --git a/src/endpoints/records.rs b/src/endpoints/records.rs index e756d03..3a09f2f 100644 --- a/src/endpoints/records.rs +++ b/src/endpoints/records.rs @@ -3,109 +3,115 @@ use reqwest::Method; use crate::client::OnspringClient; use crate::error::Result; use crate::models::{ - BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse, DataFormat, - PagedResponse, PagingRequest, QueryRecordsRequest, Record, SaveRecordRequest, - SaveRecordResponse, + BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse, DataFormat, PagedResponse, + PagingRequest, QueryRecordsRequest, Record, SaveRecordRequest, SaveRecordResponse, }; impl OnspringClient { - /// Gets a paginated collection of records for a given app. - pub async fn list_records( - &self, - app_id: i32, - paging: Option, - field_ids: Option<&[i32]>, - data_format: Option, - ) -> Result> { - let path = format!("/Records/appId/{}", app_id); - let mut query = Vec::new(); - if let Some(p) = paging { - query.push(("PageNumber", p.page_number.to_string())); - query.push(("PageSize", p.page_size.to_string())); - } - if let Some(ids) = field_ids { - let ids_str = ids - .iter() - .map(|id| id.to_string()) - .collect::>() - .join(","); - query.push(("fieldIds", ids_str)); - } - if let Some(fmt) = data_format { - query.push(("dataFormat", format!("{:?}", fmt))); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, &path, &query_refs, Option::<&()>::None) - .await + /// Gets a paginated collection of records for a given app. + pub async fn list_records( + &self, + app_id: i32, + paging: Option, + field_ids: Option<&[i32]>, + data_format: Option, + ) -> Result> { + let path = format!("/Records/appId/{}", app_id); + let mut query = Vec::new(); + if let Some(p) = paging { + query.push(("PageNumber", p.page_number.to_string())); + query.push(("PageSize", p.page_size.to_string())); } + if let Some(ids) = field_ids { + let ids_str = ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + query.push(("fieldIds", ids_str)); + } + if let Some(fmt) = data_format { + query.push(("dataFormat", format!("{:?}", fmt))); + } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, &path, &query_refs, Option::<&()>::None) + .await + } - /// Gets a record by its identifier. - pub async fn get_record( - &self, - app_id: i32, - record_id: i32, - field_ids: Option<&[i32]>, - data_format: Option, - ) -> Result { - let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); - let mut query = Vec::new(); - if let Some(ids) = field_ids { - let ids_str = ids - .iter() - .map(|id| id.to_string()) - .collect::>() - .join(","); - query.push(("fieldIds", ids_str)); - } - if let Some(fmt) = data_format { - query.push(("dataFormat", format!("{:?}", fmt))); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, &path, &query_refs, Option::<&()>::None) - .await + /// Gets a record by its identifier. + pub async fn get_record( + &self, + app_id: i32, + record_id: i32, + field_ids: Option<&[i32]>, + data_format: Option, + ) -> Result { + let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); + let mut query = Vec::new(); + if let Some(ids) = field_ids { + let ids_str = ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + query.push(("fieldIds", ids_str)); } + if let Some(fmt) = data_format { + query.push(("dataFormat", format!("{:?}", fmt))); + } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, &path, &query_refs, Option::<&()>::None) + .await + } - /// Creates or updates a record. - pub async fn save_record(&self, request: SaveRecordRequest) -> Result { - self.request(Method::PUT, "/Records", &[], Some(&request)) - .await - } + /// Creates or updates a record. + pub async fn save_record(&self, request: SaveRecordRequest) -> Result { + self + .request(Method::PUT, "/Records", &[], Some(&request)) + .await + } - /// Deletes a record by its identifier. - pub async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<()> { - let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); - self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) - .await - } + /// Deletes a record by its identifier. + pub async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<()> { + let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); + self + .request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) + .await + } - /// Gets a batch of records. - pub async fn batch_get_records( - &self, - request: BatchGetRecordsRequest, - ) -> Result> { - self.request(Method::POST, "/Records/batch-get", &[], Some(&request)) - .await - } + /// Gets a batch of records. + pub async fn batch_get_records( + &self, + request: BatchGetRecordsRequest, + ) -> Result> { + self + .request(Method::POST, "/Records/batch-get", &[], Some(&request)) + .await + } - /// Queries records using a filter expression. - pub async fn query_records( - &self, - request: QueryRecordsRequest, - paging: Option, - ) -> Result> { - let mut query = Vec::new(); - if let Some(p) = paging { - query.push(("PageNumber", p.page_number.to_string())); - query.push(("PageSize", p.page_size.to_string())); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::POST, "/Records/Query", &query_refs, Some(&request)) - .await + /// Queries records using a filter expression. + pub async fn query_records( + &self, + request: QueryRecordsRequest, + paging: Option, + ) -> Result> { + let mut query = Vec::new(); + if let Some(p) = paging { + query.push(("PageNumber", p.page_number.to_string())); + query.push(("PageSize", p.page_size.to_string())); } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::POST, "/Records/Query", &query_refs, Some(&request)) + .await + } - /// Deletes a batch of records. - pub async fn batch_delete_records(&self, request: BatchDeleteRecordsRequest) -> Result<()> { - self.request_no_content(Method::POST, "/Records/batch-delete", &[], Some(&request)) - .await - } + /// Deletes a batch of records. + pub async fn batch_delete_records(&self, request: BatchDeleteRecordsRequest) -> Result<()> { + self + .request_no_content(Method::POST, "/Records/batch-delete", &[], Some(&request)) + .await + } } diff --git a/src/endpoints/reports.rs b/src/endpoints/reports.rs index 6ca490c..27ee624 100644 --- a/src/endpoints/reports.rs +++ b/src/endpoints/reports.rs @@ -3,44 +3,46 @@ use reqwest::Method; use crate::client::OnspringClient; use crate::error::Result; use crate::models::{ - DataFormat, PagedResponse, PagingRequest, ReportData, ReportDataType, ReportInfo, + DataFormat, PagedResponse, PagingRequest, ReportData, ReportDataType, ReportInfo, }; impl OnspringClient { - /// Gets report data by report ID. - pub async fn get_report( - &self, - report_id: i32, - data_format: Option, - data_type: Option, - ) -> Result { - let path = format!("/Reports/id/{}", report_id); - let mut query = Vec::new(); - if let Some(fmt) = data_format { - query.push(("apiDataFormat", format!("{:?}", fmt))); - } - if let Some(dt) = data_type { - query.push(("dataType", format!("{:?}", dt))); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, &path, &query_refs, Option::<&()>::None) - .await + /// Gets report data by report ID. + pub async fn get_report( + &self, + report_id: i32, + data_format: Option, + data_type: Option, + ) -> Result { + let path = format!("/Reports/id/{}", report_id); + let mut query = Vec::new(); + if let Some(fmt) = data_format { + query.push(("apiDataFormat", format!("{:?}", fmt))); } + if let Some(dt) = data_type { + query.push(("dataType", format!("{:?}", dt))); + } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, &path, &query_refs, Option::<&()>::None) + .await + } - /// Gets a paginated list of reports for a given application. - pub async fn list_reports( - &self, - app_id: i32, - paging: Option, - ) -> Result> { - let path = format!("/Reports/appId/{}", app_id); - let mut query = Vec::new(); - if let Some(p) = paging { - query.push(("PageNumber", p.page_number.to_string())); - query.push(("PageSize", p.page_size.to_string())); - } - let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); - self.request(Method::GET, &path, &query_refs, Option::<&()>::None) - .await + /// Gets a paginated list of reports for a given application. + pub async fn list_reports( + &self, + app_id: i32, + paging: Option, + ) -> Result> { + let path = format!("/Reports/appId/{}", app_id); + let mut query = Vec::new(); + if let Some(p) = paging { + query.push(("PageNumber", p.page_number.to_string())); + query.push(("PageSize", p.page_size.to_string())); } + let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect(); + self + .request(Method::GET, &path, &query_refs, Option::<&()>::None) + .await + } } diff --git a/src/error.rs b/src/error.rs index 43dc37d..13640f4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,21 +1,21 @@ /// Errors that can occur when using the Onspring API client. #[derive(Debug, thiserror::Error)] pub enum OnspringError { - /// An HTTP transport error occurred. - #[error("HTTP request failed: {0}")] - Http(#[from] reqwest::Error), + /// An HTTP transport error occurred. + #[error("HTTP request failed: {0}")] + Http(#[from] reqwest::Error), - /// The API returned a non-success status code. - #[error("API error (status {status_code}): {message}")] - Api { status_code: u16, message: String }, + /// The API returned a non-success status code. + #[error("API error (status {status_code}): {message}")] + Api { status_code: u16, message: String }, - /// A serialization or deserialization error occurred. - #[error("Serialization error: {0}")] - Serialization(#[from] serde_json::Error), + /// A serialization or deserialization error occurred. + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), - /// An invalid argument was provided to an SDK method. - #[error("Invalid argument: {0}")] - InvalidArgument(String), + /// An invalid argument was provided to an SDK method. + #[error("Invalid argument: {0}")] + InvalidArgument(String), } /// A type alias for `Result`. diff --git a/src/models/app.rs b/src/models/app.rs index 8a02a23..9e80630 100644 --- a/src/models/app.rs +++ b/src/models/app.rs @@ -4,7 +4,7 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct App { - pub href: Option, - pub id: i32, - pub name: Option, + pub href: Option, + pub id: i32, + pub name: Option, } diff --git a/src/models/enums.rs b/src/models/enums.rs index aad264e..5397740 100644 --- a/src/models/enums.rs +++ b/src/models/enums.rs @@ -3,46 +3,46 @@ use serde::{Deserialize, Serialize}; /// The format of data returned by the API. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum DataFormat { - Raw, - Formatted, + Raw, + Formatted, } /// The type of report data to retrieve. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ReportDataType { - ReportData, - ChartData, + ReportData, + ChartData, } /// The type of a field's value in a record. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ValueType { - String, - Integer, - Decimal, - Date, - TimeSpan, - Guid, - StringList, - IntegerList, - GuidList, - AttachmentList, - ScoringGroupList, - FileList, + String, + Integer, + Decimal, + Date, + TimeSpan, + Guid, + StringList, + IntegerList, + GuidList, + AttachmentList, + ScoringGroupList, + FileList, } /// The output type of a formula field. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum FormulaOutputType { - Text, - Numeric, - DateAndTime, - ListValue, + Text, + Numeric, + DateAndTime, + ListValue, } /// Whether a field allows single or multiple selections. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Multiplicity { - SingleSelect, - MultiSelect, + SingleSelect, + MultiSelect, } diff --git a/src/models/field.rs b/src/models/field.rs index 05c571c..732658c 100644 --- a/src/models/field.rs +++ b/src/models/field.rs @@ -7,29 +7,29 @@ use super::enums::{FormulaOutputType, Multiplicity}; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Field { - pub id: i32, - pub app_id: i32, - pub name: Option, - #[serde(rename = "type")] - pub field_type: Option, - pub status: Option, - pub is_required: bool, - pub is_unique: bool, - pub multiplicity: Option, - pub list_id: Option, - pub values: Option>, - #[serde(rename = "outputType")] - pub output_type: Option, - pub referenced_app_id: Option, + pub id: i32, + pub app_id: i32, + pub name: Option, + #[serde(rename = "type")] + pub field_type: Option, + pub status: Option, + pub is_required: bool, + pub is_unique: bool, + pub multiplicity: Option, + pub list_id: Option, + pub values: Option>, + #[serde(rename = "outputType")] + pub output_type: Option, + pub referenced_app_id: Option, } /// Represents a value in a list field. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListFieldValue { - pub id: Uuid, - pub name: String, - pub sort_order: i32, - pub numeric_value: Option, - pub color: Option, + pub id: Uuid, + pub name: String, + pub sort_order: i32, + pub numeric_value: Option, + pub color: Option, } diff --git a/src/models/file.rs b/src/models/file.rs index 78eb2a2..78187f4 100644 --- a/src/models/file.rs +++ b/src/models/file.rs @@ -6,40 +6,40 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FileInfo { - #[serde(rename = "type")] - pub file_type: Option, - pub content_type: Option, - pub name: Option, - pub created_date: Option>, - pub modified_date: Option>, - pub owner: Option, - pub notes: Option, - pub file_href: Option, + #[serde(rename = "type")] + pub file_type: Option, + pub content_type: Option, + pub name: Option, + pub created_date: Option>, + pub modified_date: Option>, + pub owner: Option, + pub notes: Option, + pub file_href: Option, } /// The content of a downloaded file. #[derive(Debug, Clone)] pub struct FileResponse { - pub content_type: Option, - pub file_name: Option, - pub data: Bytes, + pub content_type: Option, + pub file_name: Option, + pub data: Bytes, } /// Request to upload a file. #[derive(Debug, Clone)] pub struct SaveFileRequest { - pub record_id: i32, - pub field_id: i32, - pub notes: Option, - pub modified_date: Option>, - pub file_name: String, - pub file_data: Vec, - pub content_type: String, + pub record_id: i32, + pub field_id: i32, + pub notes: Option, + pub modified_date: Option>, + pub file_name: String, + pub file_data: Vec, + pub content_type: String, } /// Response from creating a file, containing the new file ID. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreatedWithIdResponse { - pub id: i32, + pub id: i32, } diff --git a/src/models/list.rs b/src/models/list.rs index 1b7fec2..331da08 100644 --- a/src/models/list.rs +++ b/src/models/list.rs @@ -5,18 +5,18 @@ use uuid::Uuid; #[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, + #[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, + pub id: Uuid, } diff --git a/src/models/paging.rs b/src/models/paging.rs index be1aee5..f6779b6 100644 --- a/src/models/paging.rs +++ b/src/models/paging.rs @@ -3,34 +3,34 @@ use serde::Deserialize; /// Parameters for paginated API requests. #[derive(Debug, Clone)] pub struct PagingRequest { - pub page_number: i32, - pub page_size: i32, + pub page_number: i32, + pub page_size: i32, } impl Default for PagingRequest { - fn default() -> Self { - Self { - page_number: 1, - page_size: 50, - } + fn default() -> Self { + Self { + page_number: 1, + page_size: 50, } + } } /// A paginated response from the API. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PagedResponse { - pub page_number: Option, - pub page_size: Option, - pub total_pages: Option, - pub total_records: Option, - pub items: Option>, + pub page_number: Option, + pub page_size: Option, + pub total_pages: Option, + pub total_records: Option, + pub items: Option>, } /// A collection response from the API (non-paginated). #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CollectionResponse { - pub count: Option, - pub items: Option>, + pub count: Option, + pub items: Option>, } diff --git a/src/models/record.rs b/src/models/record.rs index 0727a6d..8c0bd62 100644 --- a/src/models/record.rs +++ b/src/models/record.rs @@ -8,67 +8,67 @@ use super::enums::{DataFormat, ValueType}; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Record { - pub app_id: i32, - pub record_id: i32, - pub field_data: Option>, + pub app_id: i32, + pub record_id: i32, + pub field_data: Option>, } /// Represents a single field value within a record. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RecordFieldValue { - #[serde(rename = "type")] - pub value_type: ValueType, - pub field_id: i32, - pub value: serde_json::Value, + #[serde(rename = "type")] + pub value_type: ValueType, + pub field_id: i32, + pub value: serde_json::Value, } /// Request to create or update a record. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SaveRecordRequest { - pub app_id: i32, - #[serde(skip_serializing_if = "Option::is_none")] - pub record_id: Option, - pub fields: HashMap, + pub app_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub record_id: Option, + pub fields: HashMap, } /// Response from saving a record. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveRecordResponse { - pub id: i32, - pub warnings: Option>, + pub id: i32, + pub warnings: Option>, } /// Request to query records with a filter. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueryRecordsRequest { - pub app_id: i32, - pub filter: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub field_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub data_format: Option, + pub app_id: i32, + pub filter: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub field_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_format: Option, } /// Request to get a batch of records. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BatchGetRecordsRequest { - pub app_id: i32, - pub record_ids: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub field_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub data_format: Option, + pub app_id: i32, + pub record_ids: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub field_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_format: Option, } /// Request to delete a batch of records. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BatchDeleteRecordsRequest { - pub app_id: i32, - pub record_ids: Vec, + pub app_id: i32, + pub record_ids: Vec, } diff --git a/src/models/report.rs b/src/models/report.rs index cc8370e..bfad47c 100644 --- a/src/models/report.rs +++ b/src/models/report.rs @@ -4,24 +4,24 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReportInfo { - pub app_id: i32, - pub id: i32, - pub name: Option, - pub description: Option, + pub app_id: i32, + pub id: i32, + pub name: Option, + pub description: Option, } /// Represents the data returned by a report. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReportData { - pub columns: Option>, - pub rows: Option>, + pub columns: Option>, + pub rows: Option>, } /// Represents a single row in a report. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReportRow { - pub record_id: Option, - pub cells: Option>, + pub record_id: Option, + pub cells: Option>, } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1f29dfd..cd1fb59 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,9 +2,9 @@ use onspring::OnspringClient; use wiremock::MockServer; pub async fn setup() -> (MockServer, OnspringClient) { - let mock_server = MockServer::start().await; - let client = OnspringClient::builder("test-api-key") - .base_url(mock_server.uri()) - .build(); - (mock_server, client) + let mock_server = MockServer::start().await; + let client = OnspringClient::builder("test-api-key") + .base_url(mock_server.uri()) + .build(); + (mock_server, client) } diff --git a/tests/test_apps.rs b/tests/test_apps.rs index 0ea6993..55e6d60 100644 --- a/tests/test_apps.rs +++ b/tests/test_apps.rs @@ -5,116 +5,116 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_list_apps_success() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Apps")) - .and(header("x-apikey", "test-api-key")) - .and(header("x-api-version", "2")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 1, - "pageSize": 50, - "totalPages": 1, - "totalRecords": 2, - "items": [ - {"href": "/Apps/id/1", "id": 1, "name": "App One"}, - {"href": "/Apps/id/2", "id": 2, "name": "App Two"} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Apps")) + .and(header("x-apikey", "test-api-key")) + .and(header("x-api-version", "2")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 2, + "items": [ + {"href": "/Apps/id/1", "id": 1, "name": "App One"}, + {"href": "/Apps/id/2", "id": 2, "name": "App Two"} + ] + }))) + .mount(&mock_server) + .await; - let result = client.list_apps(None).await.unwrap(); - assert_eq!(result.total_records, Some(2)); - let items = result.items.unwrap(); - assert_eq!(items.len(), 2); - assert_eq!(items[0].id, 1); - assert_eq!(items[0].name.as_deref(), Some("App One")); + let result = client.list_apps(None).await.unwrap(); + assert_eq!(result.total_records, Some(2)); + let items = result.items.unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items[0].id, 1); + assert_eq!(items[0].name.as_deref(), Some("App One")); } #[tokio::test] async fn test_list_apps_with_paging() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Apps")) - .and(query_param("PageNumber", "2")) - .and(query_param("PageSize", "10")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 2, - "pageSize": 10, - "totalPages": 3, - "totalRecords": 25, - "items": [] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Apps")) + .and(query_param("PageNumber", "2")) + .and(query_param("PageSize", "10")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 2, + "pageSize": 10, + "totalPages": 3, + "totalRecords": 25, + "items": [] + }))) + .mount(&mock_server) + .await; - let paging = onspring::PagingRequest { - page_number: 2, - page_size: 10, - }; - let result = client.list_apps(Some(paging)).await.unwrap(); - assert_eq!(result.page_number, Some(2)); + let paging = onspring::PagingRequest { + page_number: 2, + page_size: 10, + }; + let result = client.list_apps(Some(paging)).await.unwrap(); + assert_eq!(result.page_number, Some(2)); } #[tokio::test] async fn test_get_app_success() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Apps/id/42")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "href": "/Apps/id/42", - "id": 42, - "name": "Test App" - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Apps/id/42")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "href": "/Apps/id/42", + "id": 42, + "name": "Test App" + }))) + .mount(&mock_server) + .await; - let app = client.get_app(42).await.unwrap(); - assert_eq!(app.id, 42); - assert_eq!(app.name.as_deref(), Some("Test App")); + let app = client.get_app(42).await.unwrap(); + assert_eq!(app.id, 42); + assert_eq!(app.name.as_deref(), Some("Test App")); } #[tokio::test] async fn test_get_app_not_found() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Apps/id/999")) - .respond_with(ResponseTemplate::new(404)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Apps/id/999")) + .respond_with(ResponseTemplate::new(404)) + .mount(&mock_server) + .await; - let result = client.get_app(999).await; - assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { status_code, .. }) = result { - assert_eq!(status_code, 404); - } else { - panic!("Expected Api error"); - } + let result = client.get_app(999).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 404); + } else { + panic!("Expected Api error"); + } } #[tokio::test] async fn test_batch_get_apps() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) - .and(path("/Apps/batch-get")) - .and(body_json(serde_json::json!([1, 2, 3]))) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "count": 3, - "items": [ - {"href": "/Apps/id/1", "id": 1, "name": "App 1"}, - {"href": "/Apps/id/2", "id": 2, "name": "App 2"}, - {"href": "/Apps/id/3", "id": 3, "name": "App 3"} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("POST")) + .and(path("/Apps/batch-get")) + .and(body_json(serde_json::json!([1, 2, 3]))) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "count": 3, + "items": [ + {"href": "/Apps/id/1", "id": 1, "name": "App 1"}, + {"href": "/Apps/id/2", "id": 2, "name": "App 2"}, + {"href": "/Apps/id/3", "id": 3, "name": "App 3"} + ] + }))) + .mount(&mock_server) + .await; - let result = client.batch_get_apps(&[1, 2, 3]).await.unwrap(); - assert_eq!(result.count, Some(3)); - assert_eq!(result.items.unwrap().len(), 3); + let result = client.batch_get_apps(&[1, 2, 3]).await.unwrap(); + assert_eq!(result.count, Some(3)); + assert_eq!(result.items.unwrap().len(), 3); } diff --git a/tests/test_fields.rs b/tests/test_fields.rs index a9da994..ff49e88 100644 --- a/tests/test_fields.rs +++ b/tests/test_fields.rs @@ -5,75 +5,75 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_get_field_success() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Fields/id/100")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "id": 100, - "appId": 1, - "name": "Text Field", - "type": "Text", - "status": "Enabled", - "isRequired": true, - "isUnique": false - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Fields/id/100")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": 100, + "appId": 1, + "name": "Text Field", + "type": "Text", + "status": "Enabled", + "isRequired": true, + "isUnique": false + }))) + .mount(&mock_server) + .await; - let field = client.get_field(100).await.unwrap(); - assert_eq!(field.id, 100); - assert_eq!(field.app_id, 1); - assert_eq!(field.name.as_deref(), Some("Text Field")); - assert!(field.is_required); - assert!(!field.is_unique); + let field = client.get_field(100).await.unwrap(); + assert_eq!(field.id, 100); + assert_eq!(field.app_id, 1); + assert_eq!(field.name.as_deref(), Some("Text Field")); + assert!(field.is_required); + assert!(!field.is_unique); } #[tokio::test] async fn test_get_field_list_type() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Fields/id/200")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "id": 200, - "appId": 1, - "name": "Status", - "type": "List", - "status": "Enabled", - "isRequired": false, - "isUnique": false, - "multiplicity": "SingleSelect", - "listId": 50, - "values": [ - { - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "name": "Active", - "sortOrder": 1, - "numericValue": 1.0, - "color": "#00ff00" - } - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Fields/id/200")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": 200, + "appId": 1, + "name": "Status", + "type": "List", + "status": "Enabled", + "isRequired": false, + "isUnique": false, + "multiplicity": "SingleSelect", + "listId": 50, + "values": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Active", + "sortOrder": 1, + "numericValue": 1.0, + "color": "#00ff00" + } + ] + }))) + .mount(&mock_server) + .await; - let field = client.get_field(200).await.unwrap(); - assert_eq!( - field.multiplicity, - Some(onspring::Multiplicity::SingleSelect) - ); - assert_eq!(field.list_id, Some(50)); - let values = field.values.unwrap(); - assert_eq!(values.len(), 1); - assert_eq!(values[0].name, "Active"); + let field = client.get_field(200).await.unwrap(); + assert_eq!( + field.multiplicity, + Some(onspring::Multiplicity::SingleSelect) + ); + assert_eq!(field.list_id, Some(50)); + let values = field.values.unwrap(); + assert_eq!(values.len(), 1); + assert_eq!(values[0].name, "Active"); } #[tokio::test] async fn test_batch_get_fields() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) + Mock::given(method("POST")) .and(path("/Fields/batch-get")) .and(body_json(serde_json::json!([1, 2]))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ @@ -86,16 +86,16 @@ async fn test_batch_get_fields() { .mount(&mock_server) .await; - let result = client.batch_get_fields(&[1, 2]).await.unwrap(); - assert_eq!(result.count, Some(2)); - assert_eq!(result.items.unwrap().len(), 2); + let result = client.batch_get_fields(&[1, 2]).await.unwrap(); + assert_eq!(result.count, Some(2)); + assert_eq!(result.items.unwrap().len(), 2); } #[tokio::test] async fn test_list_fields_for_app() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) + Mock::given(method("GET")) .and(path("/Fields/appId/5")) .and(query_param("PageNumber", "1")) .and(query_param("PageSize", "25")) @@ -113,30 +113,30 @@ async fn test_list_fields_for_app() { .mount(&mock_server) .await; - let paging = onspring::PagingRequest { - page_number: 1, - page_size: 25, - }; - let result = client.list_fields(5, Some(paging)).await.unwrap(); - assert_eq!(result.total_records, Some(3)); - assert_eq!(result.items.unwrap().len(), 3); + let paging = onspring::PagingRequest { + page_number: 1, + page_size: 25, + }; + let result = client.list_fields(5, Some(paging)).await.unwrap(); + assert_eq!(result.total_records, Some(3)); + assert_eq!(result.items.unwrap().len(), 3); } #[tokio::test] async fn test_get_field_not_found() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Fields/id/999")) - .respond_with(ResponseTemplate::new(404)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Fields/id/999")) + .respond_with(ResponseTemplate::new(404)) + .mount(&mock_server) + .await; - let result = client.get_field(999).await; - assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { status_code, .. }) = result { - assert_eq!(status_code, 404); - } else { - panic!("Expected Api error"); - } + let result = client.get_field(999).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 404); + } else { + panic!("Expected Api error"); + } } diff --git a/tests/test_files.rs b/tests/test_files.rs index 080f1f4..b5fb70f 100644 --- a/tests/test_files.rs +++ b/tests/test_files.rs @@ -5,112 +5,112 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_get_file_info() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Files/recordId/1/fieldId/2/fileId/3")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "type": "Attachment", - "contentType": "application/pdf", - "name": "document.pdf", - "createdDate": "2024-01-15T10:30:00Z", - "modifiedDate": "2024-01-15T10:30:00Z", - "owner": "admin", - "notes": "Important doc", - "fileHref": "/Files/recordId/1/fieldId/2/fileId/3/file" - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Files/recordId/1/fieldId/2/fileId/3")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "type": "Attachment", + "contentType": "application/pdf", + "name": "document.pdf", + "createdDate": "2024-01-15T10:30:00Z", + "modifiedDate": "2024-01-15T10:30:00Z", + "owner": "admin", + "notes": "Important doc", + "fileHref": "/Files/recordId/1/fieldId/2/fileId/3/file" + }))) + .mount(&mock_server) + .await; - let info = client.get_file_info(1, 2, 3).await.unwrap(); - assert_eq!(info.name.as_deref(), Some("document.pdf")); - assert_eq!(info.content_type.as_deref(), Some("application/pdf")); - assert_eq!(info.owner.as_deref(), Some("admin")); + let info = client.get_file_info(1, 2, 3).await.unwrap(); + assert_eq!(info.name.as_deref(), Some("document.pdf")); + assert_eq!(info.content_type.as_deref(), Some("application/pdf")); + assert_eq!(info.owner.as_deref(), Some("admin")); } #[tokio::test] async fn test_get_file_content() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - let file_bytes = b"hello file content"; - Mock::given(method("GET")) - .and(path("/Files/recordId/1/fieldId/2/fileId/3/file")) - .respond_with( - ResponseTemplate::new(200) - .set_body_bytes(file_bytes.to_vec()) - .insert_header("content-type", "application/pdf") - .insert_header("content-disposition", "attachment; filename=\"test.pdf\""), - ) - .mount(&mock_server) - .await; + let file_bytes = b"hello file content"; + Mock::given(method("GET")) + .and(path("/Files/recordId/1/fieldId/2/fileId/3/file")) + .respond_with( + ResponseTemplate::new(200) + .set_body_bytes(file_bytes.to_vec()) + .insert_header("content-type", "application/pdf") + .insert_header("content-disposition", "attachment; filename=\"test.pdf\""), + ) + .mount(&mock_server) + .await; - let response = client.get_file(1, 2, 3).await.unwrap(); - assert_eq!(response.data.as_ref(), file_bytes); - assert_eq!(response.content_type.as_deref(), Some("application/pdf")); - assert_eq!(response.file_name.as_deref(), Some("test.pdf")); + let response = client.get_file(1, 2, 3).await.unwrap(); + assert_eq!(response.data.as_ref(), file_bytes); + assert_eq!(response.content_type.as_deref(), Some("application/pdf")); + assert_eq!(response.file_name.as_deref(), Some("test.pdf")); } #[tokio::test] async fn test_upload_file() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) - .and(path("/Files")) - .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ - "id": 42 - }))) - .mount(&mock_server) - .await; + Mock::given(method("POST")) + .and(path("/Files")) + .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ + "id": 42 + }))) + .mount(&mock_server) + .await; - let request = onspring::models::file::SaveFileRequest { - record_id: 1, - field_id: 2, - notes: Some("test notes".to_string()), - modified_date: None, - file_name: "test.txt".to_string(), - file_data: b"file content".to_vec(), - content_type: "text/plain".to_string(), - }; - let result = client.upload_file(request).await.unwrap(); - assert_eq!(result.id, 42); + let request = onspring::models::file::SaveFileRequest { + record_id: 1, + field_id: 2, + notes: Some("test notes".to_string()), + modified_date: None, + file_name: "test.txt".to_string(), + file_data: b"file content".to_vec(), + content_type: "text/plain".to_string(), + }; + let result = client.upload_file(request).await.unwrap(); + assert_eq!(result.id, 42); } #[tokio::test] async fn test_delete_file() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("DELETE")) - .and(path("/Files/recordId/1/fieldId/2/fileId/3")) - .respond_with(ResponseTemplate::new(204)) - .mount(&mock_server) - .await; + Mock::given(method("DELETE")) + .and(path("/Files/recordId/1/fieldId/2/fileId/3")) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; - let result = client.delete_file(1, 2, 3).await; - assert!(result.is_ok()); + let result = client.delete_file(1, 2, 3).await; + assert!(result.is_ok()); } #[tokio::test] async fn test_get_file_info_not_found() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Files/recordId/1/fieldId/2/fileId/999")) - .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({ - "message": "File not found" - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Files/recordId/1/fieldId/2/fileId/999")) + .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({ + "message": "File not found" + }))) + .mount(&mock_server) + .await; - let result = client.get_file_info(1, 2, 999).await; - assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { - status_code, - message, - }) = result - { - assert_eq!(status_code, 404); - assert_eq!(message, "File not found"); - } else { - panic!("Expected Api error"); - } + let result = client.get_file_info(1, 2, 999).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { + status_code, + message, + }) = result + { + assert_eq!(status_code, 404); + assert_eq!(message, "File not found"); + } else { + panic!("Expected Api error"); + } } diff --git a/tests/test_lists.rs b/tests/test_lists.rs index 1c42bb9..d9a0af4 100644 --- a/tests/test_lists.rs +++ b/tests/test_lists.rs @@ -6,81 +6,81 @@ 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(); + 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; + 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); + 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(); + 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; + 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); + 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(); + 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; + 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()); + 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(); + 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; + 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"); - } + 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"); + } } diff --git a/tests/test_ping.rs b/tests/test_ping.rs index 1d2e838..ded59a8 100644 --- a/tests/test_ping.rs +++ b/tests/test_ping.rs @@ -5,36 +5,36 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_ping_success() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Ping")) - .and(header("x-apikey", "test-api-key")) - .and(header("x-api-version", "2")) - .respond_with(ResponseTemplate::new(200)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Ping")) + .and(header("x-apikey", "test-api-key")) + .and(header("x-api-version", "2")) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; - let result = client.ping().await; - assert!(result.is_ok()); + let result = client.ping().await; + assert!(result.is_ok()); } #[tokio::test] async fn test_ping_unauthorized() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Ping")) - .respond_with(ResponseTemplate::new(401)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Ping")) + .respond_with(ResponseTemplate::new(401)) + .mount(&mock_server) + .await; - let result = client.ping().await; - assert!(result.is_err()); + let result = client.ping().await; + assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { status_code, .. }) = result { - assert_eq!(status_code, 401); - } else { - panic!("Expected Api error"); - } + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 401); + } else { + panic!("Expected Api error"); + } } diff --git a/tests/test_records.rs b/tests/test_records.rs index 04e252d..a13fad6 100644 --- a/tests/test_records.rs +++ b/tests/test_records.rs @@ -7,220 +7,220 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_list_records() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Records/appId/1")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 1, - "pageSize": 50, - "totalPages": 1, - "totalRecords": 1, - "items": [ - { - "appId": 1, - "recordId": 100, - "fieldData": [ - {"type": "String", "fieldId": 10, "value": "hello"} - ] - } - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Records/appId/1")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 1, + "items": [ + { + "appId": 1, + "recordId": 100, + "fieldData": [ + {"type": "String", "fieldId": 10, "value": "hello"} + ] + } + ] + }))) + .mount(&mock_server) + .await; - let result = client.list_records(1, None, None, None).await.unwrap(); - assert_eq!(result.total_records, Some(1)); - let items = result.items.unwrap(); - assert_eq!(items[0].record_id, 100); - let field_data = items[0].field_data.as_ref().unwrap(); - assert_eq!(field_data[0].field_id, 10); - assert_eq!(field_data[0].value, serde_json::json!("hello")); + let result = client.list_records(1, None, None, None).await.unwrap(); + assert_eq!(result.total_records, Some(1)); + let items = result.items.unwrap(); + assert_eq!(items[0].record_id, 100); + let field_data = items[0].field_data.as_ref().unwrap(); + assert_eq!(field_data[0].field_id, 10); + assert_eq!(field_data[0].value, serde_json::json!("hello")); } #[tokio::test] async fn test_list_records_with_params() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Records/appId/1")) - .and(query_param("fieldIds", "10,20")) - .and(query_param("dataFormat", "Formatted")) - .and(query_param("PageNumber", "2")) - .and(query_param("PageSize", "10")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 2, - "pageSize": 10, - "totalPages": 5, - "totalRecords": 50, - "items": [] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Records/appId/1")) + .and(query_param("fieldIds", "10,20")) + .and(query_param("dataFormat", "Formatted")) + .and(query_param("PageNumber", "2")) + .and(query_param("PageSize", "10")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 2, + "pageSize": 10, + "totalPages": 5, + "totalRecords": 50, + "items": [] + }))) + .mount(&mock_server) + .await; - let paging = onspring::PagingRequest { - page_number: 2, - page_size: 10, - }; - let result = client - .list_records( - 1, - Some(paging), - Some(&[10, 20]), - Some(onspring::DataFormat::Formatted), - ) - .await - .unwrap(); - assert_eq!(result.page_number, Some(2)); + let paging = onspring::PagingRequest { + page_number: 2, + page_size: 10, + }; + let result = client + .list_records( + 1, + Some(paging), + Some(&[10, 20]), + Some(onspring::DataFormat::Formatted), + ) + .await + .unwrap(); + assert_eq!(result.page_number, Some(2)); } #[tokio::test] async fn test_get_record() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Records/appId/1/recordId/42")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "appId": 1, - "recordId": 42, - "fieldData": [ - {"type": "Integer", "fieldId": 5, "value": 123} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Records/appId/1/recordId/42")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "appId": 1, + "recordId": 42, + "fieldData": [ + {"type": "Integer", "fieldId": 5, "value": 123} + ] + }))) + .mount(&mock_server) + .await; - let record = client.get_record(1, 42, None, None).await.unwrap(); - assert_eq!(record.record_id, 42); - assert_eq!(record.app_id, 1); + let record = client.get_record(1, 42, None, None).await.unwrap(); + assert_eq!(record.record_id, 42); + assert_eq!(record.app_id, 1); } #[tokio::test] async fn test_save_record_create() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("PUT")) - .and(path("/Records")) - .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ - "id": 99, - "warnings": [] - }))) - .mount(&mock_server) - .await; + Mock::given(method("PUT")) + .and(path("/Records")) + .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ + "id": 99, + "warnings": [] + }))) + .mount(&mock_server) + .await; - let mut fields = HashMap::new(); - fields.insert("10".to_string(), serde_json::json!("test value")); + let mut fields = HashMap::new(); + fields.insert("10".to_string(), serde_json::json!("test value")); - let request = onspring::models::record::SaveRecordRequest { - app_id: 1, - record_id: None, - fields, - }; - let result = client.save_record(request).await.unwrap(); - assert_eq!(result.id, 99); + let request = onspring::models::record::SaveRecordRequest { + app_id: 1, + record_id: None, + fields, + }; + let result = client.save_record(request).await.unwrap(); + assert_eq!(result.id, 99); } #[tokio::test] async fn test_delete_record() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("DELETE")) - .and(path("/Records/appId/1/recordId/42")) - .respond_with(ResponseTemplate::new(204)) - .mount(&mock_server) - .await; + Mock::given(method("DELETE")) + .and(path("/Records/appId/1/recordId/42")) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; - let result = client.delete_record(1, 42).await; - assert!(result.is_ok()); + let result = client.delete_record(1, 42).await; + assert!(result.is_ok()); } #[tokio::test] async fn test_batch_get_records() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) - .and(path("/Records/batch-get")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "count": 2, - "items": [ - {"appId": 1, "recordId": 1, "fieldData": []}, - {"appId": 1, "recordId": 2, "fieldData": []} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("POST")) + .and(path("/Records/batch-get")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "count": 2, + "items": [ + {"appId": 1, "recordId": 1, "fieldData": []}, + {"appId": 1, "recordId": 2, "fieldData": []} + ] + }))) + .mount(&mock_server) + .await; - let request = onspring::models::record::BatchGetRecordsRequest { - app_id: 1, - record_ids: vec![1, 2], - field_ids: None, - data_format: None, - }; - let result = client.batch_get_records(request).await.unwrap(); - assert_eq!(result.count, Some(2)); + let request = onspring::models::record::BatchGetRecordsRequest { + app_id: 1, + record_ids: vec![1, 2], + field_ids: None, + data_format: None, + }; + let result = client.batch_get_records(request).await.unwrap(); + assert_eq!(result.count, Some(2)); } #[tokio::test] async fn test_query_records() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) - .and(path("/Records/Query")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 1, - "pageSize": 50, - "totalPages": 1, - "totalRecords": 1, - "items": [ - {"appId": 1, "recordId": 5, "fieldData": []} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("POST")) + .and(path("/Records/Query")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 1, + "items": [ + {"appId": 1, "recordId": 5, "fieldData": []} + ] + }))) + .mount(&mock_server) + .await; - let request = onspring::models::record::QueryRecordsRequest { - app_id: 1, - filter: "fieldId eq 'test'".to_string(), - field_ids: None, - data_format: None, - }; - let result = client.query_records(request, None).await.unwrap(); - assert_eq!(result.total_records, Some(1)); + let request = onspring::models::record::QueryRecordsRequest { + app_id: 1, + filter: "fieldId eq 'test'".to_string(), + field_ids: None, + data_format: None, + }; + let result = client.query_records(request, None).await.unwrap(); + assert_eq!(result.total_records, Some(1)); } #[tokio::test] async fn test_batch_delete_records() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("POST")) - .and(path("/Records/batch-delete")) - .respond_with(ResponseTemplate::new(204)) - .mount(&mock_server) - .await; + Mock::given(method("POST")) + .and(path("/Records/batch-delete")) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; - let request = onspring::models::record::BatchDeleteRecordsRequest { - app_id: 1, - record_ids: vec![1, 2, 3], - }; - let result = client.batch_delete_records(request).await; - assert!(result.is_ok()); + let request = onspring::models::record::BatchDeleteRecordsRequest { + app_id: 1, + record_ids: vec![1, 2, 3], + }; + let result = client.batch_delete_records(request).await; + assert!(result.is_ok()); } #[tokio::test] async fn test_get_record_not_found() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Records/appId/1/recordId/999")) - .respond_with(ResponseTemplate::new(404)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Records/appId/1/recordId/999")) + .respond_with(ResponseTemplate::new(404)) + .mount(&mock_server) + .await; - let result = client.get_record(1, 999, None, None).await; - assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { status_code, .. }) = result { - assert_eq!(status_code, 404); - } else { - panic!("Expected Api error"); - } + let result = client.get_record(1, 999, None, None).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 404); + } else { + panic!("Expected Api error"); + } } diff --git a/tests/test_reports.rs b/tests/test_reports.rs index 82cce26..54aa7a1 100644 --- a/tests/test_reports.rs +++ b/tests/test_reports.rs @@ -5,97 +5,97 @@ use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn test_get_report() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Reports/id/1")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "columns": ["Record Id", "Name", "Status"], - "rows": [ - {"recordId": 100, "cells": [100, "Test", "Active"]}, - {"recordId": 101, "cells": [101, "Other", "Inactive"]} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Reports/id/1")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "columns": ["Record Id", "Name", "Status"], + "rows": [ + {"recordId": 100, "cells": [100, "Test", "Active"]}, + {"recordId": 101, "cells": [101, "Other", "Inactive"]} + ] + }))) + .mount(&mock_server) + .await; - let report = client.get_report(1, None, None).await.unwrap(); - let columns = report.columns.unwrap(); - assert_eq!(columns.len(), 3); - assert_eq!(columns[0], "Record Id"); - let rows = report.rows.unwrap(); - assert_eq!(rows.len(), 2); - assert_eq!(rows[0].record_id, Some(100)); + let report = client.get_report(1, None, None).await.unwrap(); + let columns = report.columns.unwrap(); + assert_eq!(columns.len(), 3); + assert_eq!(columns[0], "Record Id"); + let rows = report.rows.unwrap(); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].record_id, Some(100)); } #[tokio::test] async fn test_get_report_with_format() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Reports/id/1")) - .and(query_param("apiDataFormat", "Formatted")) - .and(query_param("dataType", "ChartData")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "columns": ["Name"], - "rows": [] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Reports/id/1")) + .and(query_param("apiDataFormat", "Formatted")) + .and(query_param("dataType", "ChartData")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "columns": ["Name"], + "rows": [] + }))) + .mount(&mock_server) + .await; - let result = client - .get_report( - 1, - Some(onspring::DataFormat::Formatted), - Some(onspring::ReportDataType::ChartData), - ) - .await - .unwrap(); - assert!(result.columns.unwrap().len() == 1); + let result = client + .get_report( + 1, + Some(onspring::DataFormat::Formatted), + Some(onspring::ReportDataType::ChartData), + ) + .await + .unwrap(); + assert!(result.columns.unwrap().len() == 1); } #[tokio::test] async fn test_list_reports() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Reports/appId/5")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "pageNumber": 1, - "pageSize": 50, - "totalPages": 1, - "totalRecords": 2, - "items": [ - {"appId": 5, "id": 10, "name": "Report A", "description": "First report"}, - {"appId": 5, "id": 11, "name": "Report B", "description": null} - ] - }))) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Reports/appId/5")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "pageNumber": 1, + "pageSize": 50, + "totalPages": 1, + "totalRecords": 2, + "items": [ + {"appId": 5, "id": 10, "name": "Report A", "description": "First report"}, + {"appId": 5, "id": 11, "name": "Report B", "description": null} + ] + }))) + .mount(&mock_server) + .await; - let result = client.list_reports(5, None).await.unwrap(); - assert_eq!(result.total_records, Some(2)); - let items = result.items.unwrap(); - assert_eq!(items[0].id, 10); - assert_eq!(items[0].name.as_deref(), Some("Report A")); - assert_eq!(items[1].description, None); + let result = client.list_reports(5, None).await.unwrap(); + assert_eq!(result.total_records, Some(2)); + let items = result.items.unwrap(); + assert_eq!(items[0].id, 10); + assert_eq!(items[0].name.as_deref(), Some("Report A")); + assert_eq!(items[1].description, None); } #[tokio::test] async fn test_get_report_not_found() { - let (mock_server, client) = common::setup().await; + let (mock_server, client) = common::setup().await; - Mock::given(method("GET")) - .and(path("/Reports/id/999")) - .respond_with(ResponseTemplate::new(404)) - .mount(&mock_server) - .await; + Mock::given(method("GET")) + .and(path("/Reports/id/999")) + .respond_with(ResponseTemplate::new(404)) + .mount(&mock_server) + .await; - let result = client.get_report(999, None, None).await; - assert!(result.is_err()); - if let Err(onspring::OnspringError::Api { status_code, .. }) = result { - assert_eq!(status_code, 404); - } else { - panic!("Expected Api error"); - } + let result = client.get_report(999, None, None).await; + assert!(result.is_err()); + if let Err(onspring::OnspringError::Api { status_code, .. }) = result { + assert_eq!(status_code, 404); + } else { + panic!("Expected Api error"); + } }