style: use 2-space indentation across entire codebase

Add rustfmt.toml with tab_spaces = 2 and reformat all source files,
tests, and README code examples to use 2-space indentation.
This commit is contained in:
Stevan Freeborn
2026-03-25 14:11:08 -05:00
parent 9201556646
commit 905f7f945a
27 changed files with 1250 additions and 1231 deletions
+75 -75
View File
@@ -85,8 +85,8 @@ use onspring::OnspringClient;
use std::env; use std::env;
let client = OnspringClient::builder(env::var("API_KEY").unwrap()) let client = OnspringClient::builder(env::var("API_KEY").unwrap())
.base_url(env::var("BASE_URL").unwrap()) .base_url(env::var("BASE_URL").unwrap())
.build(); .build();
``` ```
### Client Configuration ### Client Configuration
@@ -99,18 +99,18 @@ use std::time::Duration;
// Custom timeout // Custom timeout
let client = OnspringClient::builder("your-api-key") let client = OnspringClient::builder("your-api-key")
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.build(); .build();
// Custom reqwest client // Custom reqwest client
let http_client = reqwest::Client::builder() let http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(60)) .timeout(Duration::from_secs(60))
.build() .build()
.unwrap(); .unwrap();
let client = OnspringClient::builder("your-api-key") let client = OnspringClient::builder("your-api-key")
.http_client(http_client) .http_client(http_client)
.build(); .build();
``` ```
### Error Handling ### Error Handling
@@ -124,11 +124,11 @@ All client methods return `onspring::Result<T>`, which is an alias for `Result<T
```rust ```rust
match client.get_app(130).await { match client.get_app(130).await {
Ok(app) => println!("App: {}", app.name.unwrap_or_default()), Ok(app) => println!("App: {}", app.name.unwrap_or_default()),
Err(onspring::OnspringError::Api { status_code, message }) => { Err(onspring::OnspringError::Api { status_code, message }) => {
eprintln!("API error {}: {}", status_code, message); eprintln!("API error {}: {}", status_code, message);
} }
Err(e) => eprintln!("Error: {}", e), Err(e) => eprintln!("Error: {}", e),
} }
``` ```
@@ -160,7 +160,7 @@ let res = client.list_apps(None).await?;
let apps = res.items.unwrap_or_default(); let apps = res.items.unwrap_or_default();
for app in &apps { 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(); let apps = res.items.unwrap_or_default();
for app in &apps { 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(); let apps = res.items.unwrap_or_default();
for app in &apps { 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(); let fields = res.items.unwrap_or_default();
for field in &fields { 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(); let fields = res.items.unwrap_or_default();
for field in &fields { 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(); let fields = res.items.unwrap_or_default();
for field in &fields { for field in &fields {
println!("{:?}", field); println!("{:?}", field);
} }
``` ```
@@ -275,7 +275,7 @@ println!("Content-Type: {:?}", file.content_type);
println!("File Name: {:?}", file.file_name); println!("File Name: {:?}", file.file_name);
if let Some(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; use onspring::models::file::SaveFileRequest;
let request = SaveFileRequest { let request = SaveFileRequest {
record_id: 1, record_id: 1,
field_id: 4806, field_id: 4806,
notes: Some("notes".to_string()), notes: Some("notes".to_string()),
modified_date: None, modified_date: None,
file_name: "test-attachment.txt".to_string(), file_name: "test-attachment.txt".to_string(),
file_data: std::fs::read("test-attachment.txt")?, file_data: std::fs::read("test-attachment.txt")?,
content_type: "text/plain".to_string(), content_type: "text/plain".to_string(),
}; };
let res = client.upload_file(request).await?; 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; use onspring::models::list::SaveListItemRequest;
let request = SaveListItemRequest { let request = SaveListItemRequest {
id: None, id: None,
name: "New Value".to_string(), name: "New Value".to_string(),
numeric_value: Some(1.0), numeric_value: Some(1.0),
color: Some("#000000".to_string()), color: Some("#000000".to_string()),
}; };
let res = client.save_list_item(638, request).await?; 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 item_id: Uuid = "35c79a46-04b8-4069-bbc1-161a175f962c".parse().unwrap();
let request = SaveListItemRequest { let request = SaveListItemRequest {
id: Some(item_id), id: Some(item_id),
name: "Updated Value".to_string(), name: "Updated Value".to_string(),
numeric_value: Some(1.0), numeric_value: Some(1.0),
color: Some("#000000".to_string()), color: Some("#000000".to_string()),
}; };
let res = client.save_list_item(638, request).await?; 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(); let records = res.items.unwrap_or_default();
for record in &records { 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 paging = PagingRequest { page_number: 1, page_size: 10 };
let res = client.list_records( let res = client.list_records(
130, 130,
Some(paging), Some(paging),
Some(&[4804]), Some(&[4804]),
Some(DataFormat::Raw), Some(DataFormat::Raw),
).await?; ).await?;
let records = res.items.unwrap_or_default(); let records = res.items.unwrap_or_default();
for record in &records { 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; use onspring::models::record::BatchGetRecordsRequest;
let request = BatchGetRecordsRequest { let request = BatchGetRecordsRequest {
app_id: 130, app_id: 130,
record_ids: vec![1, 2], record_ids: vec![1, 2],
field_ids: None, field_ids: None,
data_format: None, data_format: None,
}; };
let res = client.batch_get_records(request).await?; let res = client.batch_get_records(request).await?;
let records = res.items.unwrap_or_default(); let records = res.items.unwrap_or_default();
for record in &records { 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}; use onspring::{DataFormat, models::record::BatchGetRecordsRequest};
let request = BatchGetRecordsRequest { let request = BatchGetRecordsRequest {
app_id: 130, app_id: 130,
record_ids: vec![1, 2], record_ids: vec![1, 2],
field_ids: Some(vec![4804]), field_ids: Some(vec![4804]),
data_format: Some(DataFormat::Formatted), data_format: Some(DataFormat::Formatted),
}; };
let res = client.batch_get_records(request).await?; let res = client.batch_get_records(request).await?;
let records = res.items.unwrap_or_default(); let records = res.items.unwrap_or_default();
for record in &records { 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; use onspring::models::record::QueryRecordsRequest;
let request = QueryRecordsRequest { let request = QueryRecordsRequest {
app_id: 130, app_id: 130,
filter: "not (4745 eq 0)".to_string(), filter: "not (4745 eq 0)".to_string(),
field_ids: None, field_ids: None,
data_format: None, data_format: None,
}; };
let res = client.query_records(request, None).await?; let res = client.query_records(request, None).await?;
let records = res.items.unwrap_or_default(); let records = res.items.unwrap_or_default();
for record in &records { 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}; use onspring::{DataFormat, PagingRequest, models::record::QueryRecordsRequest};
let request = QueryRecordsRequest { let request = QueryRecordsRequest {
app_id: 130, app_id: 130,
filter: "not (4745 eq 0)".to_string(), filter: "not (4745 eq 0)".to_string(),
field_ids: Some(vec![4804]), field_ids: Some(vec![4804]),
data_format: Some(DataFormat::Formatted), data_format: Some(DataFormat::Formatted),
}; };
let paging = PagingRequest { page_number: 1, page_size: 10 }; 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(); let records = res.items.unwrap_or_default();
for record in &records { 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")); fields.insert("4804".to_string(), serde_json::json!("Test"));
let request = SaveRecordRequest { let request = SaveRecordRequest {
app_id: 130, app_id: 130,
record_id: None, record_id: None,
fields, fields,
}; };
let res = client.save_record(request).await?; 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")); fields.insert("4804".to_string(), serde_json::json!("Updated"));
let request = SaveRecordRequest { let request = SaveRecordRequest {
app_id: 130, app_id: 130,
record_id: Some(607), record_id: Some(607),
fields, fields,
}; };
let res = client.save_record(request).await?; 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; use onspring::models::record::BatchDeleteRecordsRequest;
let request = BatchDeleteRecordsRequest { let request = BatchDeleteRecordsRequest {
app_id: 130, app_id: 130,
record_ids: vec![608, 609], record_ids: vec![608, 609],
}; };
client.batch_delete_records(request).await?; 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}; use onspring::{DataFormat, ReportDataType};
let report = client.get_report( let report = client.get_report(
409, 409,
Some(DataFormat::Formatted), Some(DataFormat::Formatted),
Some(ReportDataType::ChartData), Some(ReportDataType::ChartData),
).await?; ).await?;
println!("{:?}", report); println!("{:?}", report);
``` ```
@@ -591,7 +591,7 @@ let res = client.list_reports(130, None).await?;
let reports = res.items.unwrap_or_default(); let reports = res.items.unwrap_or_default();
for report in &reports { 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(); let reports = res.items.unwrap_or_default();
for report in &reports { for report in &reports {
println!("{:?}", report); println!("{:?}", report);
} }
``` ```
+1
View File
@@ -0,0 +1 @@
tab_spaces = 2
+180 -180
View File
@@ -10,221 +10,221 @@ use crate::error::{OnspringError, Result};
/// Client for interacting with the Onspring API v2. /// Client for interacting with the Onspring API v2.
pub struct OnspringClient { pub struct OnspringClient {
http_client: reqwest::Client, http_client: reqwest::Client,
base_url: String, base_url: String,
api_key: String, api_key: String,
} }
/// Builder for constructing an [`OnspringClient`]. /// Builder for constructing an [`OnspringClient`].
pub struct OnspringClientBuilder { pub struct OnspringClientBuilder {
base_url: String, base_url: String,
api_key: String, api_key: String,
http_client: Option<reqwest::Client>, http_client: Option<reqwest::Client>,
timeout: Option<Duration>, timeout: Option<Duration>,
} }
impl OnspringClientBuilder { impl OnspringClientBuilder {
/// Creates a new builder with the given API key. /// Creates a new builder with the given API key.
pub fn new(api_key: impl Into<String>) -> Self { pub fn new(api_key: impl Into<String>) -> Self {
Self { Self {
base_url: "https://api.onspring.com".to_string(), base_url: "https://api.onspring.com".to_string(),
api_key: api_key.into(), api_key: api_key.into(),
http_client: None, http_client: None,
timeout: None, timeout: None,
}
} }
}
/// Sets the base URL for the API. /// Sets the base URL for the API.
pub fn base_url(mut self, url: impl Into<String>) -> Self { pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into(); self.base_url = url.into();
self self
} }
/// Sets a custom `reqwest::Client` to use for HTTP requests. /// Sets a custom `reqwest::Client` to use for HTTP requests.
pub fn http_client(mut self, client: reqwest::Client) -> Self { pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client); self.http_client = Some(client);
self self
} }
/// Sets the request timeout. /// Sets the request timeout.
pub fn timeout(mut self, timeout: Duration) -> Self { pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout); self.timeout = Some(timeout);
self self
} }
/// Builds the [`OnspringClient`]. /// Builds the [`OnspringClient`].
pub fn build(self) -> OnspringClient { pub fn build(self) -> OnspringClient {
let http_client = self.http_client.unwrap_or_else(|| { let http_client = self.http_client.unwrap_or_else(|| {
reqwest::Client::builder() reqwest::Client::builder()
.timeout(self.timeout.unwrap_or(Duration::from_secs(120))) .timeout(self.timeout.unwrap_or(Duration::from_secs(120)))
.build() .build()
.expect("failed to build HTTP client") .expect("failed to build HTTP client")
}); });
OnspringClient { OnspringClient {
http_client, http_client,
base_url: self.base_url.trim_end_matches('/').to_string(), base_url: self.base_url.trim_end_matches('/').to_string(),
api_key: self.api_key, api_key: self.api_key,
}
} }
}
} }
impl OnspringClient { impl OnspringClient {
/// Creates a new [`OnspringClientBuilder`]. /// Creates a new [`OnspringClientBuilder`].
pub fn builder(api_key: impl Into<String>) -> OnspringClientBuilder { pub fn builder(api_key: impl Into<String>) -> OnspringClientBuilder {
OnspringClientBuilder::new(api_key) 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<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
query: &[(&str, String)],
body: Option<&impl Serialize>,
) -> Result<T> {
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 response = req.send().await?;
let mut headers = HeaderMap::new(); let status = response.status();
headers.insert(
"x-apikey", if !status.is_success() {
HeaderValue::from_str(&self.api_key).expect("invalid API key"), let message = response
); .json::<serde_json::Value>()
headers.insert("x-api-version", HeaderValue::from_static("2")); .await
headers .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<T: DeserializeOwned>( let body = response.text().await?;
&self, serde_json::from_str(&body).map_err(OnspringError::Serialization)
method: Method, }
path: &str,
query: &[(&str, String)],
body: Option<&impl Serialize>,
) -> Result<T> {
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 { pub(crate) async fn request_no_content(
req = req.json(body); &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?; if let Some(body) = body {
let status = response.status(); req = req.json(body);
if !status.is_success() {
let message = response
.json::<serde_json::Value>()
.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)
} }
pub(crate) async fn request_no_content( let response = req.send().await?;
&self, let status = response.status();
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 { if !status.is_success() {
req = req.json(body); let message = response
} .json::<serde_json::Value>()
.await
let response = req.send().await?; .ok()
let status = response.status(); .and_then(|v| v.get("message")?.as_str().map(String::from))
.unwrap_or_default();
if !status.is_success() { return Err(OnspringError::Api {
let message = response status_code: status.as_u16(),
.json::<serde_json::Value>() message,
.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(())
} }
pub(crate) async fn request_bytes( Ok(())
&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?;
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 status = response.status();
let message = response
.json::<serde_json::Value>()
.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 headers = response.headers().clone(); if !status.is_success() {
let bytes = response.bytes().await?; let message = response
Ok((status, headers, bytes)) .json::<serde_json::Value>()
.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<T: DeserializeOwned>( let headers = response.headers().clone();
&self, let bytes = response.bytes().await?;
path: &str, Ok((status, headers, bytes))
form: reqwest::multipart::Form, }
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let response = self
.http_client
.post(&url)
.headers(self.default_headers())
.multipart(form)
.send()
.await?;
let status = response.status(); pub(crate) async fn request_multipart<T: DeserializeOwned>(
&self,
path: &str,
form: reqwest::multipart::Form,
) -> Result<T> {
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 status = response.status();
let message = response
.json::<serde_json::Value>()
.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?; if !status.is_success() {
serde_json::from_str(&body).map_err(OnspringError::Serialization) let message = response
.json::<serde_json::Value>()
.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)
}
} }
+24 -21
View File
@@ -5,28 +5,31 @@ use crate::error::Result;
use crate::models::{App, CollectionResponse, PagedResponse, PagingRequest}; use crate::models::{App, CollectionResponse, PagedResponse, PagingRequest};
impl OnspringClient { impl OnspringClient {
/// Gets all apps for the current client, with optional pagination. /// Gets all apps for the current client, with optional pagination.
pub async fn list_apps(&self, paging: Option<PagingRequest>) -> Result<PagedResponse<App>> { pub async fn list_apps(&self, paging: Option<PagingRequest>) -> Result<PagedResponse<App>> {
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(p) = paging { if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string())); query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.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
} }
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. /// Gets an app by its identifier.
pub async fn get_app(&self, app_id: i32) -> Result<App> { pub async fn get_app(&self, app_id: i32) -> Result<App> {
let path = format!("/Apps/id/{}", app_id); let path = format!("/Apps/id/{}", app_id);
self.request(Method::GET, &path, &[], Option::<&()>::None) self
.await .request(Method::GET, &path, &[], Option::<&()>::None)
} .await
}
/// Gets up to 100 apps by their identifiers. /// Gets up to 100 apps by their identifiers.
pub async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>> { pub async fn batch_get_apps(&self, ids: &[i32]) -> Result<CollectionResponse<App>> {
self.request(Method::POST, "/Apps/batch-get", &[], Some(&ids)) self
.await .request(Method::POST, "/Apps/batch-get", &[], Some(&ids))
} .await
}
} }
+29 -26
View File
@@ -5,33 +5,36 @@ use crate::error::Result;
use crate::models::{CollectionResponse, Field, PagedResponse, PagingRequest}; use crate::models::{CollectionResponse, Field, PagedResponse, PagingRequest};
impl OnspringClient { impl OnspringClient {
/// Gets a field by its identifier. /// Gets a field by its identifier.
pub async fn get_field(&self, field_id: i32) -> Result<Field> { pub async fn get_field(&self, field_id: i32) -> Result<Field> {
let path = format!("/Fields/id/{}", field_id); let path = format!("/Fields/id/{}", field_id);
self.request(Method::GET, &path, &[], Option::<&()>::None) self
.await .request(Method::GET, &path, &[], Option::<&()>::None)
} .await
}
/// Gets up to 100 fields by their identifiers. /// Gets up to 100 fields by their identifiers.
pub async fn batch_get_fields(&self, ids: &[i32]) -> Result<CollectionResponse<Field>> { pub async fn batch_get_fields(&self, ids: &[i32]) -> Result<CollectionResponse<Field>> {
self.request(Method::POST, "/Fields/batch-get", &[], Some(&ids)) self
.await .request(Method::POST, "/Fields/batch-get", &[], Some(&ids))
} .await
}
/// Gets a paginated list of fields for a given application. /// Gets a paginated list of fields for a given application.
pub async fn list_fields( pub async fn list_fields(
&self, &self,
app_id: i32, app_id: i32,
paging: Option<PagingRequest>, paging: Option<PagingRequest>,
) -> Result<PagedResponse<Field>> { ) -> Result<PagedResponse<Field>> {
let path = format!("/Fields/appId/{}", app_id); let path = format!("/Fields/appId/{}", app_id);
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(p) = paging { if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string())); query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.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
} }
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
}
} }
+79 -77
View File
@@ -5,84 +5,86 @@ use crate::error::Result;
use crate::models::{CreatedWithIdResponse, FileInfo, FileResponse, SaveFileRequest}; use crate::models::{CreatedWithIdResponse, FileInfo, FileResponse, SaveFileRequest};
impl OnspringClient { impl OnspringClient {
/// Gets a file's metadata information. /// Gets a file's metadata information.
pub async fn get_file_info( pub async fn get_file_info(
&self, &self,
record_id: i32, record_id: i32,
field_id: i32, field_id: i32,
file_id: i32, file_id: i32,
) -> Result<FileInfo> { ) -> Result<FileInfo> {
let path = format!( let path = format!(
"/Files/recordId/{}/fieldId/{}/fileId/{}", "/Files/recordId/{}/fieldId/{}/fileId/{}",
record_id, field_id, file_id record_id, field_id, file_id
); );
self.request(Method::GET, &path, &[], Option::<&()>::None) self
.await .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<FileResponse> {
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<CreatedWithIdResponse> {
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. self.request_multipart("/Files", form).await
pub async fn get_file( }
&self,
record_id: i32,
field_id: i32,
file_id: i32,
) -> Result<FileResponse> {
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 /// Deletes a file attachment.
.get("content-type") pub async fn delete_file(&self, record_id: i32, field_id: i32, file_id: i32) -> Result<()> {
.and_then(|v| v.to_str().ok()) let path = format!(
.map(String::from); "/Files/recordId/{}/fieldId/{}/fileId/{}",
record_id, field_id, file_id
let file_name = headers );
.get("content-disposition") self
.and_then(|v| v.to_str().ok()) .request_no_content(Method::DELETE, &path, &[], Option::<&()>::None)
.and_then(|v| { .await
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<CreatedWithIdResponse> {
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
}
} }
+16 -15
View File
@@ -6,20 +6,21 @@ use crate::error::Result;
use crate::models::{SaveListItemRequest, SaveListItemResponse}; use crate::models::{SaveListItemRequest, SaveListItemResponse};
impl OnspringClient { impl OnspringClient {
/// Creates or updates a list item in the specified list. /// Creates or updates a list item in the specified list.
pub async fn save_list_item( pub async fn save_list_item(
&self, &self,
list_id: i32, list_id: i32,
request: SaveListItemRequest, request: SaveListItemRequest,
) -> Result<SaveListItemResponse> { ) -> Result<SaveListItemResponse> {
let path = format!("/Lists/id/{}/items", list_id); let path = format!("/Lists/id/{}/items", list_id);
self.request(Method::PUT, &path, &[], Some(&request)).await self.request(Method::PUT, &path, &[], Some(&request)).await
} }
/// Deletes a list item from the specified list. /// Deletes a list item from the specified list.
pub async fn delete_list_item(&self, list_id: i32, item_id: Uuid) -> Result<()> { pub async fn delete_list_item(&self, list_id: i32, item_id: Uuid) -> Result<()> {
let path = format!("/Lists/id/{}/itemId/{}", list_id, item_id); let path = format!("/Lists/id/{}/itemId/{}", list_id, item_id);
self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) self
.await .request_no_content(Method::DELETE, &path, &[], Option::<&()>::None)
} .await
}
} }
+6 -5
View File
@@ -4,9 +4,10 @@ use crate::client::OnspringClient;
use crate::error::Result; use crate::error::Result;
impl OnspringClient { impl OnspringClient {
/// Checks if the Onspring API is reachable. /// Checks if the Onspring API is reachable.
pub async fn ping(&self) -> Result<()> { pub async fn ping(&self) -> Result<()> {
self.request_no_content(Method::GET, "/Ping", &[], Option::<&()>::None) self
.await .request_no_content(Method::GET, "/Ping", &[], Option::<&()>::None)
} .await
}
} }
+99 -93
View File
@@ -3,109 +3,115 @@ use reqwest::Method;
use crate::client::OnspringClient; use crate::client::OnspringClient;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse, DataFormat, BatchDeleteRecordsRequest, BatchGetRecordsRequest, CollectionResponse, DataFormat, PagedResponse,
PagedResponse, PagingRequest, QueryRecordsRequest, Record, SaveRecordRequest, PagingRequest, QueryRecordsRequest, Record, SaveRecordRequest, SaveRecordResponse,
SaveRecordResponse,
}; };
impl OnspringClient { impl OnspringClient {
/// Gets a paginated collection of records for a given app. /// Gets a paginated collection of records for a given app.
pub async fn list_records( pub async fn list_records(
&self, &self,
app_id: i32, app_id: i32,
paging: Option<PagingRequest>, paging: Option<PagingRequest>,
field_ids: Option<&[i32]>, field_ids: Option<&[i32]>,
data_format: Option<DataFormat>, data_format: Option<DataFormat>,
) -> Result<PagedResponse<Record>> { ) -> Result<PagedResponse<Record>> {
let path = format!("/Records/appId/{}", app_id); let path = format!("/Records/appId/{}", app_id);
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(p) = paging { if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string())); query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.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::<Vec<_>>()
.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
} }
if let Some(ids) = field_ids {
let ids_str = ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.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. /// Gets a record by its identifier.
pub async fn get_record( pub async fn get_record(
&self, &self,
app_id: i32, app_id: i32,
record_id: i32, record_id: i32,
field_ids: Option<&[i32]>, field_ids: Option<&[i32]>,
data_format: Option<DataFormat>, data_format: Option<DataFormat>,
) -> Result<Record> { ) -> Result<Record> {
let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id);
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(ids) = field_ids { if let Some(ids) = field_ids {
let ids_str = ids let ids_str = ids
.iter() .iter()
.map(|id| id.to_string()) .map(|id| id.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
query.push(("fieldIds", ids_str)); 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
} }
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. /// Creates or updates a record.
pub async fn save_record(&self, request: SaveRecordRequest) -> Result<SaveRecordResponse> { pub async fn save_record(&self, request: SaveRecordRequest) -> Result<SaveRecordResponse> {
self.request(Method::PUT, "/Records", &[], Some(&request)) self
.await .request(Method::PUT, "/Records", &[], Some(&request))
} .await
}
/// Deletes a record by its identifier. /// Deletes a record by its identifier.
pub async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<()> { pub async fn delete_record(&self, app_id: i32, record_id: i32) -> Result<()> {
let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id); let path = format!("/Records/appId/{}/recordId/{}", app_id, record_id);
self.request_no_content(Method::DELETE, &path, &[], Option::<&()>::None) self
.await .request_no_content(Method::DELETE, &path, &[], Option::<&()>::None)
} .await
}
/// Gets a batch of records. /// Gets a batch of records.
pub async fn batch_get_records( pub async fn batch_get_records(
&self, &self,
request: BatchGetRecordsRequest, request: BatchGetRecordsRequest,
) -> Result<CollectionResponse<Record>> { ) -> Result<CollectionResponse<Record>> {
self.request(Method::POST, "/Records/batch-get", &[], Some(&request)) self
.await .request(Method::POST, "/Records/batch-get", &[], Some(&request))
} .await
}
/// Queries records using a filter expression. /// Queries records using a filter expression.
pub async fn query_records( pub async fn query_records(
&self, &self,
request: QueryRecordsRequest, request: QueryRecordsRequest,
paging: Option<PagingRequest>, paging: Option<PagingRequest>,
) -> Result<PagedResponse<Record>> { ) -> Result<PagedResponse<Record>> {
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(p) = paging { if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string())); query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.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
} }
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. /// Deletes a batch of records.
pub async fn batch_delete_records(&self, request: BatchDeleteRecordsRequest) -> Result<()> { pub async fn batch_delete_records(&self, request: BatchDeleteRecordsRequest) -> Result<()> {
self.request_no_content(Method::POST, "/Records/batch-delete", &[], Some(&request)) self
.await .request_no_content(Method::POST, "/Records/batch-delete", &[], Some(&request))
} .await
}
} }
+36 -34
View File
@@ -3,44 +3,46 @@ use reqwest::Method;
use crate::client::OnspringClient; use crate::client::OnspringClient;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
DataFormat, PagedResponse, PagingRequest, ReportData, ReportDataType, ReportInfo, DataFormat, PagedResponse, PagingRequest, ReportData, ReportDataType, ReportInfo,
}; };
impl OnspringClient { impl OnspringClient {
/// Gets report data by report ID. /// Gets report data by report ID.
pub async fn get_report( pub async fn get_report(
&self, &self,
report_id: i32, report_id: i32,
data_format: Option<DataFormat>, data_format: Option<DataFormat>,
data_type: Option<ReportDataType>, data_type: Option<ReportDataType>,
) -> Result<ReportData> { ) -> Result<ReportData> {
let path = format!("/Reports/id/{}", report_id); let path = format!("/Reports/id/{}", report_id);
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(fmt) = data_format { if let Some(fmt) = data_format {
query.push(("apiDataFormat", format!("{:?}", fmt))); 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
} }
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. /// Gets a paginated list of reports for a given application.
pub async fn list_reports( pub async fn list_reports(
&self, &self,
app_id: i32, app_id: i32,
paging: Option<PagingRequest>, paging: Option<PagingRequest>,
) -> Result<PagedResponse<ReportInfo>> { ) -> Result<PagedResponse<ReportInfo>> {
let path = format!("/Reports/appId/{}", app_id); let path = format!("/Reports/appId/{}", app_id);
let mut query = Vec::new(); let mut query = Vec::new();
if let Some(p) = paging { if let Some(p) = paging {
query.push(("PageNumber", p.page_number.to_string())); query.push(("PageNumber", p.page_number.to_string()));
query.push(("PageSize", p.page_size.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
} }
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
}
} }
+12 -12
View File
@@ -1,21 +1,21 @@
/// Errors that can occur when using the Onspring API client. /// Errors that can occur when using the Onspring API client.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum OnspringError { pub enum OnspringError {
/// An HTTP transport error occurred. /// An HTTP transport error occurred.
#[error("HTTP request failed: {0}")] #[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error), Http(#[from] reqwest::Error),
/// The API returned a non-success status code. /// The API returned a non-success status code.
#[error("API error (status {status_code}): {message}")] #[error("API error (status {status_code}): {message}")]
Api { status_code: u16, message: String }, Api { status_code: u16, message: String },
/// A serialization or deserialization error occurred. /// A serialization or deserialization error occurred.
#[error("Serialization error: {0}")] #[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error), Serialization(#[from] serde_json::Error),
/// An invalid argument was provided to an SDK method. /// An invalid argument was provided to an SDK method.
#[error("Invalid argument: {0}")] #[error("Invalid argument: {0}")]
InvalidArgument(String), InvalidArgument(String),
} }
/// A type alias for `Result<T, OnspringError>`. /// A type alias for `Result<T, OnspringError>`.
+3 -3
View File
@@ -4,7 +4,7 @@ use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct App { pub struct App {
pub href: Option<String>, pub href: Option<String>,
pub id: i32, pub id: i32,
pub name: Option<String>, pub name: Option<String>,
} }
+22 -22
View File
@@ -3,46 +3,46 @@ use serde::{Deserialize, Serialize};
/// The format of data returned by the API. /// The format of data returned by the API.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataFormat { pub enum DataFormat {
Raw, Raw,
Formatted, Formatted,
} }
/// The type of report data to retrieve. /// The type of report data to retrieve.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReportDataType { pub enum ReportDataType {
ReportData, ReportData,
ChartData, ChartData,
} }
/// The type of a field's value in a record. /// The type of a field's value in a record.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValueType { pub enum ValueType {
String, String,
Integer, Integer,
Decimal, Decimal,
Date, Date,
TimeSpan, TimeSpan,
Guid, Guid,
StringList, StringList,
IntegerList, IntegerList,
GuidList, GuidList,
AttachmentList, AttachmentList,
ScoringGroupList, ScoringGroupList,
FileList, FileList,
} }
/// The output type of a formula field. /// The output type of a formula field.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FormulaOutputType { pub enum FormulaOutputType {
Text, Text,
Numeric, Numeric,
DateAndTime, DateAndTime,
ListValue, ListValue,
} }
/// Whether a field allows single or multiple selections. /// Whether a field allows single or multiple selections.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Multiplicity { pub enum Multiplicity {
SingleSelect, SingleSelect,
MultiSelect, MultiSelect,
} }
+19 -19
View File
@@ -7,29 +7,29 @@ use super::enums::{FormulaOutputType, Multiplicity};
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Field { pub struct Field {
pub id: i32, pub id: i32,
pub app_id: i32, pub app_id: i32,
pub name: Option<String>, pub name: Option<String>,
#[serde(rename = "type")] #[serde(rename = "type")]
pub field_type: Option<String>, pub field_type: Option<String>,
pub status: Option<String>, pub status: Option<String>,
pub is_required: bool, pub is_required: bool,
pub is_unique: bool, pub is_unique: bool,
pub multiplicity: Option<Multiplicity>, pub multiplicity: Option<Multiplicity>,
pub list_id: Option<i32>, pub list_id: Option<i32>,
pub values: Option<Vec<ListFieldValue>>, pub values: Option<Vec<ListFieldValue>>,
#[serde(rename = "outputType")] #[serde(rename = "outputType")]
pub output_type: Option<FormulaOutputType>, pub output_type: Option<FormulaOutputType>,
pub referenced_app_id: Option<i32>, pub referenced_app_id: Option<i32>,
} }
/// Represents a value in a list field. /// Represents a value in a list field.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ListFieldValue { pub struct ListFieldValue {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub sort_order: i32, pub sort_order: i32,
pub numeric_value: Option<f64>, pub numeric_value: Option<f64>,
pub color: Option<String>, pub color: Option<String>,
} }
+20 -20
View File
@@ -6,40 +6,40 @@ use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct FileInfo { pub struct FileInfo {
#[serde(rename = "type")] #[serde(rename = "type")]
pub file_type: Option<String>, pub file_type: Option<String>,
pub content_type: Option<String>, pub content_type: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub created_date: Option<DateTime<Utc>>, pub created_date: Option<DateTime<Utc>>,
pub modified_date: Option<DateTime<Utc>>, pub modified_date: Option<DateTime<Utc>>,
pub owner: Option<String>, pub owner: Option<String>,
pub notes: Option<String>, pub notes: Option<String>,
pub file_href: Option<String>, pub file_href: Option<String>,
} }
/// The content of a downloaded file. /// The content of a downloaded file.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileResponse { pub struct FileResponse {
pub content_type: Option<String>, pub content_type: Option<String>,
pub file_name: Option<String>, pub file_name: Option<String>,
pub data: Bytes, pub data: Bytes,
} }
/// Request to upload a file. /// Request to upload a file.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SaveFileRequest { pub struct SaveFileRequest {
pub record_id: i32, pub record_id: i32,
pub field_id: i32, pub field_id: i32,
pub notes: Option<String>, pub notes: Option<String>,
pub modified_date: Option<DateTime<Utc>>, pub modified_date: Option<DateTime<Utc>>,
pub file_name: String, pub file_name: String,
pub file_data: Vec<u8>, pub file_data: Vec<u8>,
pub content_type: String, pub content_type: String,
} }
/// Response from creating a file, containing the new file ID. /// Response from creating a file, containing the new file ID.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CreatedWithIdResponse { pub struct CreatedWithIdResponse {
pub id: i32, pub id: i32,
} }
+8 -8
View File
@@ -5,18 +5,18 @@ use uuid::Uuid;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SaveListItemRequest { pub struct SaveListItemRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Uuid>, pub id: Option<Uuid>,
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub numeric_value: Option<f64>, pub numeric_value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>, pub color: Option<String>,
} }
/// Response from saving a list item. /// Response from saving a list item.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SaveListItemResponse { pub struct SaveListItemResponse {
pub id: Uuid, pub id: Uuid,
} }
+14 -14
View File
@@ -3,34 +3,34 @@ use serde::Deserialize;
/// Parameters for paginated API requests. /// Parameters for paginated API requests.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PagingRequest { pub struct PagingRequest {
pub page_number: i32, pub page_number: i32,
pub page_size: i32, pub page_size: i32,
} }
impl Default for PagingRequest { impl Default for PagingRequest {
fn default() -> Self { fn default() -> Self {
Self { Self {
page_number: 1, page_number: 1,
page_size: 50, page_size: 50,
}
} }
}
} }
/// A paginated response from the API. /// A paginated response from the API.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PagedResponse<T> { pub struct PagedResponse<T> {
pub page_number: Option<i32>, pub page_number: Option<i32>,
pub page_size: Option<i32>, pub page_size: Option<i32>,
pub total_pages: Option<i32>, pub total_pages: Option<i32>,
pub total_records: Option<i32>, pub total_records: Option<i32>,
pub items: Option<Vec<T>>, pub items: Option<Vec<T>>,
} }
/// A collection response from the API (non-paginated). /// A collection response from the API (non-paginated).
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CollectionResponse<T> { pub struct CollectionResponse<T> {
pub count: Option<i32>, pub count: Option<i32>,
pub items: Option<Vec<T>>, pub items: Option<Vec<T>>,
} }
+27 -27
View File
@@ -8,67 +8,67 @@ use super::enums::{DataFormat, ValueType};
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Record { pub struct Record {
pub app_id: i32, pub app_id: i32,
pub record_id: i32, pub record_id: i32,
pub field_data: Option<Vec<RecordFieldValue>>, pub field_data: Option<Vec<RecordFieldValue>>,
} }
/// Represents a single field value within a record. /// Represents a single field value within a record.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RecordFieldValue { pub struct RecordFieldValue {
#[serde(rename = "type")] #[serde(rename = "type")]
pub value_type: ValueType, pub value_type: ValueType,
pub field_id: i32, pub field_id: i32,
pub value: serde_json::Value, pub value: serde_json::Value,
} }
/// Request to create or update a record. /// Request to create or update a record.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SaveRecordRequest { pub struct SaveRecordRequest {
pub app_id: i32, pub app_id: i32,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub record_id: Option<i32>, pub record_id: Option<i32>,
pub fields: HashMap<String, serde_json::Value>, pub fields: HashMap<String, serde_json::Value>,
} }
/// Response from saving a record. /// Response from saving a record.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SaveRecordResponse { pub struct SaveRecordResponse {
pub id: i32, pub id: i32,
pub warnings: Option<Vec<String>>, pub warnings: Option<Vec<String>>,
} }
/// Request to query records with a filter. /// Request to query records with a filter.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct QueryRecordsRequest { pub struct QueryRecordsRequest {
pub app_id: i32, pub app_id: i32,
pub filter: String, pub filter: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub field_ids: Option<Vec<i32>>, pub field_ids: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub data_format: Option<DataFormat>, pub data_format: Option<DataFormat>,
} }
/// Request to get a batch of records. /// Request to get a batch of records.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BatchGetRecordsRequest { pub struct BatchGetRecordsRequest {
pub app_id: i32, pub app_id: i32,
pub record_ids: Vec<i32>, pub record_ids: Vec<i32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub field_ids: Option<Vec<i32>>, pub field_ids: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub data_format: Option<DataFormat>, pub data_format: Option<DataFormat>,
} }
/// Request to delete a batch of records. /// Request to delete a batch of records.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BatchDeleteRecordsRequest { pub struct BatchDeleteRecordsRequest {
pub app_id: i32, pub app_id: i32,
pub record_ids: Vec<i32>, pub record_ids: Vec<i32>,
} }
+8 -8
View File
@@ -4,24 +4,24 @@ use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ReportInfo { pub struct ReportInfo {
pub app_id: i32, pub app_id: i32,
pub id: i32, pub id: i32,
pub name: Option<String>, pub name: Option<String>,
pub description: Option<String>, pub description: Option<String>,
} }
/// Represents the data returned by a report. /// Represents the data returned by a report.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ReportData { pub struct ReportData {
pub columns: Option<Vec<String>>, pub columns: Option<Vec<String>>,
pub rows: Option<Vec<ReportRow>>, pub rows: Option<Vec<ReportRow>>,
} }
/// Represents a single row in a report. /// Represents a single row in a report.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ReportRow { pub struct ReportRow {
pub record_id: Option<i32>, pub record_id: Option<i32>,
pub cells: Option<Vec<serde_json::Value>>, pub cells: Option<Vec<serde_json::Value>>,
} }
+5 -5
View File
@@ -2,9 +2,9 @@ use onspring::OnspringClient;
use wiremock::MockServer; use wiremock::MockServer;
pub async fn setup() -> (MockServer, OnspringClient) { pub async fn setup() -> (MockServer, OnspringClient) {
let mock_server = MockServer::start().await; let mock_server = MockServer::start().await;
let client = OnspringClient::builder("test-api-key") let client = OnspringClient::builder("test-api-key")
.base_url(mock_server.uri()) .base_url(mock_server.uri())
.build(); .build();
(mock_server, client) (mock_server, client)
} }
+86 -86
View File
@@ -5,116 +5,116 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_list_apps_success() { async fn test_list_apps_success() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Apps")) .and(path("/Apps"))
.and(header("x-apikey", "test-api-key")) .and(header("x-apikey", "test-api-key"))
.and(header("x-api-version", "2")) .and(header("x-api-version", "2"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 1, "pageNumber": 1,
"pageSize": 50, "pageSize": 50,
"totalPages": 1, "totalPages": 1,
"totalRecords": 2, "totalRecords": 2,
"items": [ "items": [
{"href": "/Apps/id/1", "id": 1, "name": "App One"}, {"href": "/Apps/id/1", "id": 1, "name": "App One"},
{"href": "/Apps/id/2", "id": 2, "name": "App Two"} {"href": "/Apps/id/2", "id": 2, "name": "App Two"}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.list_apps(None).await.unwrap(); let result = client.list_apps(None).await.unwrap();
assert_eq!(result.total_records, Some(2)); assert_eq!(result.total_records, Some(2));
let items = result.items.unwrap(); let items = result.items.unwrap();
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
assert_eq!(items[0].id, 1); assert_eq!(items[0].id, 1);
assert_eq!(items[0].name.as_deref(), Some("App One")); assert_eq!(items[0].name.as_deref(), Some("App One"));
} }
#[tokio::test] #[tokio::test]
async fn test_list_apps_with_paging() { 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")) Mock::given(method("GET"))
.and(path("/Apps")) .and(path("/Apps"))
.and(query_param("PageNumber", "2")) .and(query_param("PageNumber", "2"))
.and(query_param("PageSize", "10")) .and(query_param("PageSize", "10"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 2, "pageNumber": 2,
"pageSize": 10, "pageSize": 10,
"totalPages": 3, "totalPages": 3,
"totalRecords": 25, "totalRecords": 25,
"items": [] "items": []
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let paging = onspring::PagingRequest { let paging = onspring::PagingRequest {
page_number: 2, page_number: 2,
page_size: 10, page_size: 10,
}; };
let result = client.list_apps(Some(paging)).await.unwrap(); let result = client.list_apps(Some(paging)).await.unwrap();
assert_eq!(result.page_number, Some(2)); assert_eq!(result.page_number, Some(2));
} }
#[tokio::test] #[tokio::test]
async fn test_get_app_success() { async fn test_get_app_success() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Apps/id/42")) .and(path("/Apps/id/42"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"href": "/Apps/id/42", "href": "/Apps/id/42",
"id": 42, "id": 42,
"name": "Test App" "name": "Test App"
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let app = client.get_app(42).await.unwrap(); let app = client.get_app(42).await.unwrap();
assert_eq!(app.id, 42); assert_eq!(app.id, 42);
assert_eq!(app.name.as_deref(), Some("Test App")); assert_eq!(app.name.as_deref(), Some("Test App"));
} }
#[tokio::test] #[tokio::test]
async fn test_get_app_not_found() { 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")) Mock::given(method("GET"))
.and(path("/Apps/id/999")) .and(path("/Apps/id/999"))
.respond_with(ResponseTemplate::new(404)) .respond_with(ResponseTemplate::new(404))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.get_app(999).await; let result = client.get_app(999).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
#[tokio::test] #[tokio::test]
async fn test_batch_get_apps() { async fn test_batch_get_apps() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path("/Apps/batch-get")) .and(path("/Apps/batch-get"))
.and(body_json(serde_json::json!([1, 2, 3]))) .and(body_json(serde_json::json!([1, 2, 3])))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"count": 3, "count": 3,
"items": [ "items": [
{"href": "/Apps/id/1", "id": 1, "name": "App 1"}, {"href": "/Apps/id/1", "id": 1, "name": "App 1"},
{"href": "/Apps/id/2", "id": 2, "name": "App 2"}, {"href": "/Apps/id/2", "id": 2, "name": "App 2"},
{"href": "/Apps/id/3", "id": 3, "name": "App 3"} {"href": "/Apps/id/3", "id": 3, "name": "App 3"}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.batch_get_apps(&[1, 2, 3]).await.unwrap(); let result = client.batch_get_apps(&[1, 2, 3]).await.unwrap();
assert_eq!(result.count, Some(3)); assert_eq!(result.count, Some(3));
assert_eq!(result.items.unwrap().len(), 3); assert_eq!(result.items.unwrap().len(), 3);
} }
+81 -81
View File
@@ -5,75 +5,75 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_get_field_success() { async fn test_get_field_success() {
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/id/100")) .and(path("/Fields/id/100"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 100, "id": 100,
"appId": 1, "appId": 1,
"name": "Text Field", "name": "Text Field",
"type": "Text", "type": "Text",
"status": "Enabled", "status": "Enabled",
"isRequired": true, "isRequired": true,
"isUnique": false "isUnique": false
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let field = client.get_field(100).await.unwrap(); let field = client.get_field(100).await.unwrap();
assert_eq!(field.id, 100); assert_eq!(field.id, 100);
assert_eq!(field.app_id, 1); assert_eq!(field.app_id, 1);
assert_eq!(field.name.as_deref(), Some("Text Field")); assert_eq!(field.name.as_deref(), Some("Text Field"));
assert!(field.is_required); assert!(field.is_required);
assert!(!field.is_unique); assert!(!field.is_unique);
} }
#[tokio::test] #[tokio::test]
async fn test_get_field_list_type() { 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")) Mock::given(method("GET"))
.and(path("/Fields/id/200")) .and(path("/Fields/id/200"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 200, "id": 200,
"appId": 1, "appId": 1,
"name": "Status", "name": "Status",
"type": "List", "type": "List",
"status": "Enabled", "status": "Enabled",
"isRequired": false, "isRequired": false,
"isUnique": false, "isUnique": false,
"multiplicity": "SingleSelect", "multiplicity": "SingleSelect",
"listId": 50, "listId": 50,
"values": [ "values": [
{ {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Active", "name": "Active",
"sortOrder": 1, "sortOrder": 1,
"numericValue": 1.0, "numericValue": 1.0,
"color": "#00ff00" "color": "#00ff00"
} }
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let field = client.get_field(200).await.unwrap(); let field = client.get_field(200).await.unwrap();
assert_eq!( assert_eq!(
field.multiplicity, field.multiplicity,
Some(onspring::Multiplicity::SingleSelect) Some(onspring::Multiplicity::SingleSelect)
); );
assert_eq!(field.list_id, Some(50)); assert_eq!(field.list_id, Some(50));
let values = field.values.unwrap(); let values = field.values.unwrap();
assert_eq!(values.len(), 1); assert_eq!(values.len(), 1);
assert_eq!(values[0].name, "Active"); assert_eq!(values[0].name, "Active");
} }
#[tokio::test] #[tokio::test]
async fn test_batch_get_fields() { 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(path("/Fields/batch-get"))
.and(body_json(serde_json::json!([1, 2]))) .and(body_json(serde_json::json!([1, 2])))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
@@ -86,16 +86,16 @@ async fn test_batch_get_fields() {
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.batch_get_fields(&[1, 2]).await.unwrap(); let result = client.batch_get_fields(&[1, 2]).await.unwrap();
assert_eq!(result.count, Some(2)); assert_eq!(result.count, Some(2));
assert_eq!(result.items.unwrap().len(), 2); assert_eq!(result.items.unwrap().len(), 2);
} }
#[tokio::test] #[tokio::test]
async fn test_list_fields_for_app() { 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(path("/Fields/appId/5"))
.and(query_param("PageNumber", "1")) .and(query_param("PageNumber", "1"))
.and(query_param("PageSize", "25")) .and(query_param("PageSize", "25"))
@@ -113,30 +113,30 @@ async fn test_list_fields_for_app() {
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let paging = onspring::PagingRequest { let paging = onspring::PagingRequest {
page_number: 1, page_number: 1,
page_size: 25, page_size: 25,
}; };
let result = client.list_fields(5, Some(paging)).await.unwrap(); let result = client.list_fields(5, Some(paging)).await.unwrap();
assert_eq!(result.total_records, Some(3)); assert_eq!(result.total_records, Some(3));
assert_eq!(result.items.unwrap().len(), 3); assert_eq!(result.items.unwrap().len(), 3);
} }
#[tokio::test] #[tokio::test]
async fn test_get_field_not_found() { 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")) Mock::given(method("GET"))
.and(path("/Fields/id/999")) .and(path("/Fields/id/999"))
.respond_with(ResponseTemplate::new(404)) .respond_with(ResponseTemplate::new(404))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.get_field(999).await; let result = client.get_field(999).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
+82 -82
View File
@@ -5,112 +5,112 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_get_file_info() { async fn test_get_file_info() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Files/recordId/1/fieldId/2/fileId/3")) .and(path("/Files/recordId/1/fieldId/2/fileId/3"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "Attachment", "type": "Attachment",
"contentType": "application/pdf", "contentType": "application/pdf",
"name": "document.pdf", "name": "document.pdf",
"createdDate": "2024-01-15T10:30:00Z", "createdDate": "2024-01-15T10:30:00Z",
"modifiedDate": "2024-01-15T10:30:00Z", "modifiedDate": "2024-01-15T10:30:00Z",
"owner": "admin", "owner": "admin",
"notes": "Important doc", "notes": "Important doc",
"fileHref": "/Files/recordId/1/fieldId/2/fileId/3/file" "fileHref": "/Files/recordId/1/fieldId/2/fileId/3/file"
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let info = client.get_file_info(1, 2, 3).await.unwrap(); let info = client.get_file_info(1, 2, 3).await.unwrap();
assert_eq!(info.name.as_deref(), Some("document.pdf")); assert_eq!(info.name.as_deref(), Some("document.pdf"));
assert_eq!(info.content_type.as_deref(), Some("application/pdf")); assert_eq!(info.content_type.as_deref(), Some("application/pdf"));
assert_eq!(info.owner.as_deref(), Some("admin")); assert_eq!(info.owner.as_deref(), Some("admin"));
} }
#[tokio::test] #[tokio::test]
async fn test_get_file_content() { 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"; let file_bytes = b"hello file content";
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Files/recordId/1/fieldId/2/fileId/3/file")) .and(path("/Files/recordId/1/fieldId/2/fileId/3/file"))
.respond_with( .respond_with(
ResponseTemplate::new(200) ResponseTemplate::new(200)
.set_body_bytes(file_bytes.to_vec()) .set_body_bytes(file_bytes.to_vec())
.insert_header("content-type", "application/pdf") .insert_header("content-type", "application/pdf")
.insert_header("content-disposition", "attachment; filename=\"test.pdf\""), .insert_header("content-disposition", "attachment; filename=\"test.pdf\""),
) )
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let response = client.get_file(1, 2, 3).await.unwrap(); let response = client.get_file(1, 2, 3).await.unwrap();
assert_eq!(response.data.as_ref(), file_bytes); assert_eq!(response.data.as_ref(), file_bytes);
assert_eq!(response.content_type.as_deref(), Some("application/pdf")); assert_eq!(response.content_type.as_deref(), Some("application/pdf"));
assert_eq!(response.file_name.as_deref(), Some("test.pdf")); assert_eq!(response.file_name.as_deref(), Some("test.pdf"));
} }
#[tokio::test] #[tokio::test]
async fn test_upload_file() { async fn test_upload_file() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path("/Files")) .and(path("/Files"))
.respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
"id": 42 "id": 42
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::file::SaveFileRequest { let request = onspring::models::file::SaveFileRequest {
record_id: 1, record_id: 1,
field_id: 2, field_id: 2,
notes: Some("test notes".to_string()), notes: Some("test notes".to_string()),
modified_date: None, modified_date: None,
file_name: "test.txt".to_string(), file_name: "test.txt".to_string(),
file_data: b"file content".to_vec(), file_data: b"file content".to_vec(),
content_type: "text/plain".to_string(), content_type: "text/plain".to_string(),
}; };
let result = client.upload_file(request).await.unwrap(); let result = client.upload_file(request).await.unwrap();
assert_eq!(result.id, 42); assert_eq!(result.id, 42);
} }
#[tokio::test] #[tokio::test]
async fn test_delete_file() { async fn test_delete_file() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("DELETE")) Mock::given(method("DELETE"))
.and(path("/Files/recordId/1/fieldId/2/fileId/3")) .and(path("/Files/recordId/1/fieldId/2/fileId/3"))
.respond_with(ResponseTemplate::new(204)) .respond_with(ResponseTemplate::new(204))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.delete_file(1, 2, 3).await; let result = client.delete_file(1, 2, 3).await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_get_file_info_not_found() { 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")) Mock::given(method("GET"))
.and(path("/Files/recordId/1/fieldId/2/fileId/999")) .and(path("/Files/recordId/1/fieldId/2/fileId/999"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"message": "File not found" "message": "File not found"
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.get_file_info(1, 2, 999).await; let result = client.get_file_info(1, 2, 999).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { if let Err(onspring::OnspringError::Api {
status_code, status_code,
message, message,
}) = result }) = result
{ {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
assert_eq!(message, "File not found"); assert_eq!(message, "File not found");
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
+57 -57
View File
@@ -6,81 +6,81 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_save_list_item_create() { async fn test_save_list_item_create() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
let new_id = Uuid::new_v4(); let new_id = Uuid::new_v4();
Mock::given(method("PUT")) Mock::given(method("PUT"))
.and(path("/Lists/id/10/items")) .and(path("/Lists/id/10/items"))
.respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
"id": new_id.to_string() "id": new_id.to_string()
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::list::SaveListItemRequest { let request = onspring::models::list::SaveListItemRequest {
id: None, id: None,
name: "New Item".to_string(), name: "New Item".to_string(),
numeric_value: Some(1.0), numeric_value: Some(1.0),
color: Some("#ff0000".to_string()), color: Some("#ff0000".to_string()),
}; };
let result = client.save_list_item(10, request).await.unwrap(); let result = client.save_list_item(10, request).await.unwrap();
assert_eq!(result.id, new_id); assert_eq!(result.id, new_id);
} }
#[tokio::test] #[tokio::test]
async fn test_save_list_item_update() { async fn test_save_list_item_update() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
let existing_id = Uuid::new_v4(); let existing_id = Uuid::new_v4();
Mock::given(method("PUT")) Mock::given(method("PUT"))
.and(path("/Lists/id/10/items")) .and(path("/Lists/id/10/items"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": existing_id.to_string() "id": existing_id.to_string()
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::list::SaveListItemRequest { let request = onspring::models::list::SaveListItemRequest {
id: Some(existing_id), id: Some(existing_id),
name: "Updated Item".to_string(), name: "Updated Item".to_string(),
numeric_value: None, numeric_value: None,
color: None, color: None,
}; };
let result = client.save_list_item(10, request).await.unwrap(); let result = client.save_list_item(10, request).await.unwrap();
assert_eq!(result.id, existing_id); assert_eq!(result.id, existing_id);
} }
#[tokio::test] #[tokio::test]
async fn test_delete_list_item() { async fn test_delete_list_item() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
let item_id = Uuid::new_v4(); let item_id = Uuid::new_v4();
Mock::given(method("DELETE")) Mock::given(method("DELETE"))
.and(path(format!("/Lists/id/10/itemId/{}", item_id))) .and(path(format!("/Lists/id/10/itemId/{}", item_id)))
.respond_with(ResponseTemplate::new(204)) .respond_with(ResponseTemplate::new(204))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.delete_list_item(10, item_id).await; let result = client.delete_list_item(10, item_id).await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_delete_list_item_not_found() { async fn test_delete_list_item_not_found() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
let item_id = Uuid::new_v4(); let item_id = Uuid::new_v4();
Mock::given(method("DELETE")) Mock::given(method("DELETE"))
.and(path(format!("/Lists/id/10/itemId/{}", item_id))) .and(path(format!("/Lists/id/10/itemId/{}", item_id)))
.respond_with(ResponseTemplate::new(404)) .respond_with(ResponseTemplate::new(404))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.delete_list_item(10, item_id).await; let result = client.delete_list_item(10, item_id).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
+23 -23
View File
@@ -5,36 +5,36 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_ping_success() { async fn test_ping_success() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Ping")) .and(path("/Ping"))
.and(header("x-apikey", "test-api-key")) .and(header("x-apikey", "test-api-key"))
.and(header("x-api-version", "2")) .and(header("x-api-version", "2"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.ping().await; let result = client.ping().await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_ping_unauthorized() { async fn test_ping_unauthorized() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Ping")) .and(path("/Ping"))
.respond_with(ResponseTemplate::new(401)) .respond_with(ResponseTemplate::new(401))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.ping().await; let result = client.ping().await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 401); assert_eq!(status_code, 401);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
+165 -165
View File
@@ -7,220 +7,220 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_list_records() { async fn test_list_records() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Records/appId/1")) .and(path("/Records/appId/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 1, "pageNumber": 1,
"pageSize": 50, "pageSize": 50,
"totalPages": 1, "totalPages": 1,
"totalRecords": 1, "totalRecords": 1,
"items": [ "items": [
{ {
"appId": 1, "appId": 1,
"recordId": 100, "recordId": 100,
"fieldData": [ "fieldData": [
{"type": "String", "fieldId": 10, "value": "hello"} {"type": "String", "fieldId": 10, "value": "hello"}
] ]
} }
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.list_records(1, None, None, None).await.unwrap(); let result = client.list_records(1, None, None, None).await.unwrap();
assert_eq!(result.total_records, Some(1)); assert_eq!(result.total_records, Some(1));
let items = result.items.unwrap(); let items = result.items.unwrap();
assert_eq!(items[0].record_id, 100); assert_eq!(items[0].record_id, 100);
let field_data = items[0].field_data.as_ref().unwrap(); let field_data = items[0].field_data.as_ref().unwrap();
assert_eq!(field_data[0].field_id, 10); assert_eq!(field_data[0].field_id, 10);
assert_eq!(field_data[0].value, serde_json::json!("hello")); assert_eq!(field_data[0].value, serde_json::json!("hello"));
} }
#[tokio::test] #[tokio::test]
async fn test_list_records_with_params() { 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")) Mock::given(method("GET"))
.and(path("/Records/appId/1")) .and(path("/Records/appId/1"))
.and(query_param("fieldIds", "10,20")) .and(query_param("fieldIds", "10,20"))
.and(query_param("dataFormat", "Formatted")) .and(query_param("dataFormat", "Formatted"))
.and(query_param("PageNumber", "2")) .and(query_param("PageNumber", "2"))
.and(query_param("PageSize", "10")) .and(query_param("PageSize", "10"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 2, "pageNumber": 2,
"pageSize": 10, "pageSize": 10,
"totalPages": 5, "totalPages": 5,
"totalRecords": 50, "totalRecords": 50,
"items": [] "items": []
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let paging = onspring::PagingRequest { let paging = onspring::PagingRequest {
page_number: 2, page_number: 2,
page_size: 10, page_size: 10,
}; };
let result = client let result = client
.list_records( .list_records(
1, 1,
Some(paging), Some(paging),
Some(&[10, 20]), Some(&[10, 20]),
Some(onspring::DataFormat::Formatted), Some(onspring::DataFormat::Formatted),
) )
.await .await
.unwrap(); .unwrap();
assert_eq!(result.page_number, Some(2)); assert_eq!(result.page_number, Some(2));
} }
#[tokio::test] #[tokio::test]
async fn test_get_record() { async fn test_get_record() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Records/appId/1/recordId/42")) .and(path("/Records/appId/1/recordId/42"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"appId": 1, "appId": 1,
"recordId": 42, "recordId": 42,
"fieldData": [ "fieldData": [
{"type": "Integer", "fieldId": 5, "value": 123} {"type": "Integer", "fieldId": 5, "value": 123}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let record = client.get_record(1, 42, None, None).await.unwrap(); let record = client.get_record(1, 42, None, None).await.unwrap();
assert_eq!(record.record_id, 42); assert_eq!(record.record_id, 42);
assert_eq!(record.app_id, 1); assert_eq!(record.app_id, 1);
} }
#[tokio::test] #[tokio::test]
async fn test_save_record_create() { async fn test_save_record_create() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("PUT")) Mock::given(method("PUT"))
.and(path("/Records")) .and(path("/Records"))
.respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
"id": 99, "id": 99,
"warnings": [] "warnings": []
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let mut fields = HashMap::new(); let mut fields = HashMap::new();
fields.insert("10".to_string(), serde_json::json!("test value")); fields.insert("10".to_string(), serde_json::json!("test value"));
let request = onspring::models::record::SaveRecordRequest { let request = onspring::models::record::SaveRecordRequest {
app_id: 1, app_id: 1,
record_id: None, record_id: None,
fields, fields,
}; };
let result = client.save_record(request).await.unwrap(); let result = client.save_record(request).await.unwrap();
assert_eq!(result.id, 99); assert_eq!(result.id, 99);
} }
#[tokio::test] #[tokio::test]
async fn test_delete_record() { async fn test_delete_record() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("DELETE")) Mock::given(method("DELETE"))
.and(path("/Records/appId/1/recordId/42")) .and(path("/Records/appId/1/recordId/42"))
.respond_with(ResponseTemplate::new(204)) .respond_with(ResponseTemplate::new(204))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.delete_record(1, 42).await; let result = client.delete_record(1, 42).await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_batch_get_records() { async fn test_batch_get_records() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path("/Records/batch-get")) .and(path("/Records/batch-get"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"count": 2, "count": 2,
"items": [ "items": [
{"appId": 1, "recordId": 1, "fieldData": []}, {"appId": 1, "recordId": 1, "fieldData": []},
{"appId": 1, "recordId": 2, "fieldData": []} {"appId": 1, "recordId": 2, "fieldData": []}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::record::BatchGetRecordsRequest { let request = onspring::models::record::BatchGetRecordsRequest {
app_id: 1, app_id: 1,
record_ids: vec![1, 2], record_ids: vec![1, 2],
field_ids: None, field_ids: None,
data_format: None, data_format: None,
}; };
let result = client.batch_get_records(request).await.unwrap(); let result = client.batch_get_records(request).await.unwrap();
assert_eq!(result.count, Some(2)); assert_eq!(result.count, Some(2));
} }
#[tokio::test] #[tokio::test]
async fn test_query_records() { async fn test_query_records() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path("/Records/Query")) .and(path("/Records/Query"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 1, "pageNumber": 1,
"pageSize": 50, "pageSize": 50,
"totalPages": 1, "totalPages": 1,
"totalRecords": 1, "totalRecords": 1,
"items": [ "items": [
{"appId": 1, "recordId": 5, "fieldData": []} {"appId": 1, "recordId": 5, "fieldData": []}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::record::QueryRecordsRequest { let request = onspring::models::record::QueryRecordsRequest {
app_id: 1, app_id: 1,
filter: "fieldId eq 'test'".to_string(), filter: "fieldId eq 'test'".to_string(),
field_ids: None, field_ids: None,
data_format: None, data_format: None,
}; };
let result = client.query_records(request, None).await.unwrap(); let result = client.query_records(request, None).await.unwrap();
assert_eq!(result.total_records, Some(1)); assert_eq!(result.total_records, Some(1));
} }
#[tokio::test] #[tokio::test]
async fn test_batch_delete_records() { async fn test_batch_delete_records() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path("/Records/batch-delete")) .and(path("/Records/batch-delete"))
.respond_with(ResponseTemplate::new(204)) .respond_with(ResponseTemplate::new(204))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let request = onspring::models::record::BatchDeleteRecordsRequest { let request = onspring::models::record::BatchDeleteRecordsRequest {
app_id: 1, app_id: 1,
record_ids: vec![1, 2, 3], record_ids: vec![1, 2, 3],
}; };
let result = client.batch_delete_records(request).await; let result = client.batch_delete_records(request).await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_get_record_not_found() { 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")) Mock::given(method("GET"))
.and(path("/Records/appId/1/recordId/999")) .and(path("/Records/appId/1/recordId/999"))
.respond_with(ResponseTemplate::new(404)) .respond_with(ResponseTemplate::new(404))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.get_record(1, 999, None, None).await; let result = client.get_record(1, 999, None, None).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }
+73 -73
View File
@@ -5,97 +5,97 @@ use wiremock::{Mock, ResponseTemplate};
#[tokio::test] #[tokio::test]
async fn test_get_report() { async fn test_get_report() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Reports/id/1")) .and(path("/Reports/id/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"columns": ["Record Id", "Name", "Status"], "columns": ["Record Id", "Name", "Status"],
"rows": [ "rows": [
{"recordId": 100, "cells": [100, "Test", "Active"]}, {"recordId": 100, "cells": [100, "Test", "Active"]},
{"recordId": 101, "cells": [101, "Other", "Inactive"]} {"recordId": 101, "cells": [101, "Other", "Inactive"]}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let report = client.get_report(1, None, None).await.unwrap(); let report = client.get_report(1, None, None).await.unwrap();
let columns = report.columns.unwrap(); let columns = report.columns.unwrap();
assert_eq!(columns.len(), 3); assert_eq!(columns.len(), 3);
assert_eq!(columns[0], "Record Id"); assert_eq!(columns[0], "Record Id");
let rows = report.rows.unwrap(); let rows = report.rows.unwrap();
assert_eq!(rows.len(), 2); assert_eq!(rows.len(), 2);
assert_eq!(rows[0].record_id, Some(100)); assert_eq!(rows[0].record_id, Some(100));
} }
#[tokio::test] #[tokio::test]
async fn test_get_report_with_format() { 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")) Mock::given(method("GET"))
.and(path("/Reports/id/1")) .and(path("/Reports/id/1"))
.and(query_param("apiDataFormat", "Formatted")) .and(query_param("apiDataFormat", "Formatted"))
.and(query_param("dataType", "ChartData")) .and(query_param("dataType", "ChartData"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"columns": ["Name"], "columns": ["Name"],
"rows": [] "rows": []
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client let result = client
.get_report( .get_report(
1, 1,
Some(onspring::DataFormat::Formatted), Some(onspring::DataFormat::Formatted),
Some(onspring::ReportDataType::ChartData), Some(onspring::ReportDataType::ChartData),
) )
.await .await
.unwrap(); .unwrap();
assert!(result.columns.unwrap().len() == 1); assert!(result.columns.unwrap().len() == 1);
} }
#[tokio::test] #[tokio::test]
async fn test_list_reports() { async fn test_list_reports() {
let (mock_server, client) = common::setup().await; let (mock_server, client) = common::setup().await;
Mock::given(method("GET")) Mock::given(method("GET"))
.and(path("/Reports/appId/5")) .and(path("/Reports/appId/5"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"pageNumber": 1, "pageNumber": 1,
"pageSize": 50, "pageSize": 50,
"totalPages": 1, "totalPages": 1,
"totalRecords": 2, "totalRecords": 2,
"items": [ "items": [
{"appId": 5, "id": 10, "name": "Report A", "description": "First report"}, {"appId": 5, "id": 10, "name": "Report A", "description": "First report"},
{"appId": 5, "id": 11, "name": "Report B", "description": null} {"appId": 5, "id": 11, "name": "Report B", "description": null}
] ]
}))) })))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.list_reports(5, None).await.unwrap(); let result = client.list_reports(5, None).await.unwrap();
assert_eq!(result.total_records, Some(2)); assert_eq!(result.total_records, Some(2));
let items = result.items.unwrap(); let items = result.items.unwrap();
assert_eq!(items[0].id, 10); assert_eq!(items[0].id, 10);
assert_eq!(items[0].name.as_deref(), Some("Report A")); assert_eq!(items[0].name.as_deref(), Some("Report A"));
assert_eq!(items[1].description, None); assert_eq!(items[1].description, None);
} }
#[tokio::test] #[tokio::test]
async fn test_get_report_not_found() { 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")) Mock::given(method("GET"))
.and(path("/Reports/id/999")) .and(path("/Reports/id/999"))
.respond_with(ResponseTemplate::new(404)) .respond_with(ResponseTemplate::new(404))
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let result = client.get_report(999, None, None).await; let result = client.get_report(999, None, None).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(onspring::OnspringError::Api { status_code, .. }) = result { if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
assert_eq!(status_code, 404); assert_eq!(status_code, 404);
} else { } else {
panic!("Expected Api error"); panic!("Expected Api error");
} }
} }