feat: add reports endpoint with get report and list reports methods
Implement ReportInfo, ReportData, and ReportRow models. Add get_report (with optional data format and data type params) and list_reports (paginated) methods. Tests cover report data retrieval, format parameters, listing reports by app, and not-found error.
This commit is contained in:
@@ -4,3 +4,4 @@ mod files;
|
|||||||
mod lists;
|
mod lists;
|
||||||
mod ping;
|
mod ping;
|
||||||
mod records;
|
mod records;
|
||||||
|
mod reports;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use reqwest::Method;
|
||||||
|
|
||||||
|
use crate::client::OnspringClient;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::models::{
|
||||||
|
DataFormat, PagedResponse, PagingRequest, ReportData, ReportDataType, ReportInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl OnspringClient {
|
||||||
|
/// Gets report data by report ID.
|
||||||
|
pub async fn get_report(
|
||||||
|
&self,
|
||||||
|
report_id: i32,
|
||||||
|
data_format: Option<DataFormat>,
|
||||||
|
data_type: Option<ReportDataType>,
|
||||||
|
) -> Result<ReportData> {
|
||||||
|
let path = format!("/Reports/id/{}", report_id);
|
||||||
|
let mut query = Vec::new();
|
||||||
|
if let Some(fmt) = data_format {
|
||||||
|
query.push(("apiDataFormat", format!("{:?}", fmt)));
|
||||||
|
}
|
||||||
|
if let Some(dt) = data_type {
|
||||||
|
query.push(("dataType", format!("{:?}", dt)));
|
||||||
|
}
|
||||||
|
let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect();
|
||||||
|
self.request(Method::GET, &path, &query_refs, Option::<&()>::None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a paginated list of reports for a given application.
|
||||||
|
pub async fn list_reports(
|
||||||
|
&self,
|
||||||
|
app_id: i32,
|
||||||
|
paging: Option<PagingRequest>,
|
||||||
|
) -> Result<PagedResponse<ReportInfo>> {
|
||||||
|
let path = format!("/Reports/appId/{}", app_id);
|
||||||
|
let mut query = Vec::new();
|
||||||
|
if let Some(p) = paging {
|
||||||
|
query.push(("PageNumber", p.page_number.to_string()));
|
||||||
|
query.push(("PageSize", p.page_size.to_string()));
|
||||||
|
}
|
||||||
|
let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect();
|
||||||
|
self.request(Method::GET, &path, &query_refs, Option::<&()>::None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod file;
|
|||||||
pub mod list;
|
pub mod list;
|
||||||
mod paging;
|
mod paging;
|
||||||
pub mod record;
|
pub mod record;
|
||||||
|
mod report;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use enums::*;
|
pub use enums::*;
|
||||||
@@ -13,3 +14,4 @@ pub use file::*;
|
|||||||
pub use list::*;
|
pub use list::*;
|
||||||
pub use paging::*;
|
pub use paging::*;
|
||||||
pub use record::*;
|
pub use record::*;
|
||||||
|
pub use report::*;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Represents a report associated to an app (used in list responses).
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportInfo {
|
||||||
|
pub app_id: i32,
|
||||||
|
pub id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the data returned by a report.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportData {
|
||||||
|
pub columns: Option<Vec<String>>,
|
||||||
|
pub rows: Option<Vec<ReportRow>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a single row in a report.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReportRow {
|
||||||
|
pub record_id: Option<i32>,
|
||||||
|
pub cells: Option<Vec<serde_json::Value>>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use wiremock::matchers::{method, path, query_param};
|
||||||
|
use wiremock::{Mock, ResponseTemplate};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_report() {
|
||||||
|
let (mock_server, client) = common::setup().await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/Reports/id/1"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"columns": ["Record Id", "Name", "Status"],
|
||||||
|
"rows": [
|
||||||
|
{"recordId": 100, "cells": [100, "Test", "Active"]},
|
||||||
|
{"recordId": 101, "cells": [101, "Other", "Inactive"]}
|
||||||
|
]
|
||||||
|
})))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let report = client.get_report(1, None, None).await.unwrap();
|
||||||
|
let columns = report.columns.unwrap();
|
||||||
|
assert_eq!(columns.len(), 3);
|
||||||
|
assert_eq!(columns[0], "Record Id");
|
||||||
|
let rows = report.rows.unwrap();
|
||||||
|
assert_eq!(rows.len(), 2);
|
||||||
|
assert_eq!(rows[0].record_id, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_report_with_format() {
|
||||||
|
let (mock_server, client) = common::setup().await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/Reports/id/1"))
|
||||||
|
.and(query_param("apiDataFormat", "Formatted"))
|
||||||
|
.and(query_param("dataType", "ChartData"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"columns": ["Name"],
|
||||||
|
"rows": []
|
||||||
|
})))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.get_report(
|
||||||
|
1,
|
||||||
|
Some(onspring::DataFormat::Formatted),
|
||||||
|
Some(onspring::ReportDataType::ChartData),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(result.columns.unwrap().len() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_reports() {
|
||||||
|
let (mock_server, client) = common::setup().await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/Reports/appId/5"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"pageNumber": 1,
|
||||||
|
"pageSize": 50,
|
||||||
|
"totalPages": 1,
|
||||||
|
"totalRecords": 2,
|
||||||
|
"items": [
|
||||||
|
{"appId": 5, "id": 10, "name": "Report A", "description": "First report"},
|
||||||
|
{"appId": 5, "id": 11, "name": "Report B", "description": null}
|
||||||
|
]
|
||||||
|
})))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let result = client.list_reports(5, None).await.unwrap();
|
||||||
|
assert_eq!(result.total_records, Some(2));
|
||||||
|
let items = result.items.unwrap();
|
||||||
|
assert_eq!(items[0].id, 10);
|
||||||
|
assert_eq!(items[0].name.as_deref(), Some("Report A"));
|
||||||
|
assert_eq!(items[1].description, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_report_not_found() {
|
||||||
|
let (mock_server, client) = common::setup().await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/Reports/id/999"))
|
||||||
|
.respond_with(ResponseTemplate::new(404))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let result = client.get_report(999, None, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let Err(onspring::OnspringError::Api { status_code, .. }) = result {
|
||||||
|
assert_eq!(status_code, 404);
|
||||||
|
} else {
|
||||||
|
panic!("Expected Api error");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user