use crate::{CloudClient, Result, tasks::TaskStateUpdate};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CostReportFormat {
#[default]
Csv,
Json,
}
impl std::fmt::Display for CostReportFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CostReportFormat::Csv => write!(f, "csv"),
CostReportFormat::Json => write!(f, "json"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SubscriptionType {
Pro,
Essentials,
}
impl std::fmt::Display for SubscriptionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubscriptionType::Pro => write!(f, "pro"),
SubscriptionType::Essentials => write!(f, "essentials"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
pub key: String,
pub value: String,
}
impl Tag {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CostReportCreateRequest {
pub start_date: String,
pub end_date: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<CostReportFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscription_ids: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub database_ids: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subscription_type: Option<SubscriptionType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub regions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Tag>>,
}
impl CostReportCreateRequest {
#[must_use]
pub fn builder() -> CostReportCreateRequestBuilder {
CostReportCreateRequestBuilder::default()
}
pub fn new(start_date: impl Into<String>, end_date: impl Into<String>) -> Self {
Self {
start_date: start_date.into(),
end_date: end_date.into(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CostReportCreateRequestBuilder {
start_date: Option<String>,
end_date: Option<String>,
format: Option<CostReportFormat>,
subscription_ids: Option<Vec<i32>>,
database_ids: Option<Vec<i32>>,
subscription_type: Option<SubscriptionType>,
regions: Option<Vec<String>>,
tags: Option<Vec<Tag>>,
}
impl CostReportCreateRequestBuilder {
#[must_use]
pub fn start_date(mut self, date: impl Into<String>) -> Self {
self.start_date = Some(date.into());
self
}
#[must_use]
pub fn end_date(mut self, date: impl Into<String>) -> Self {
self.end_date = Some(date.into());
self
}
#[must_use]
pub fn format(mut self, format: CostReportFormat) -> Self {
self.format = Some(format);
self
}
#[must_use]
pub fn subscription_ids(mut self, ids: Vec<i32>) -> Self {
self.subscription_ids = Some(ids);
self
}
#[must_use]
pub fn database_ids(mut self, ids: Vec<i32>) -> Self {
self.database_ids = Some(ids);
self
}
#[must_use]
pub fn subscription_type(mut self, sub_type: SubscriptionType) -> Self {
self.subscription_type = Some(sub_type);
self
}
#[must_use]
pub fn regions(mut self, regions: Vec<String>) -> Self {
self.regions = Some(regions);
self
}
#[must_use]
pub fn tags(mut self, tags: Vec<Tag>) -> Self {
self.tags = Some(tags);
self
}
#[must_use]
pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let tag = Tag::new(key, value);
match &mut self.tags {
Some(tags) => tags.push(tag),
None => self.tags = Some(vec![tag]),
}
self
}
pub fn build(self) -> Result<CostReportCreateRequest> {
let start_date = self
.start_date
.ok_or_else(|| crate::CloudError::BadRequest {
message: "start_date is required".to_string(),
})?;
let end_date = self.end_date.ok_or_else(|| crate::CloudError::BadRequest {
message: "end_date is required".to_string(),
})?;
Ok(CostReportCreateRequest {
start_date,
end_date,
format: self.format,
subscription_ids: self.subscription_ids,
database_ids: self.database_ids,
subscription_type: self.subscription_type,
regions: self.regions,
tags: self.tags,
})
}
}
pub struct CostReportHandler {
client: CloudClient,
}
impl CostReportHandler {
#[must_use]
pub fn new(client: CloudClient) -> Self {
Self { client }
}
pub async fn generate_cost_report(
&self,
request: CostReportCreateRequest,
) -> Result<TaskStateUpdate> {
self.client.post("/cost-report", &request).await
}
pub async fn generate_cost_report_raw(
&self,
request: CostReportCreateRequest,
) -> Result<Value> {
let body = serde_json::to_value(request).map_err(crate::CloudError::from)?;
self.client.post_raw("/cost-report", body).await
}
pub async fn download_cost_report(&self, cost_report_id: &str) -> Result<Vec<u8>> {
self.client
.get_bytes(&format!("/cost-report/{cost_report_id}"))
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cost_report_request_builder() {
let request = CostReportCreateRequest::builder()
.start_date("2025-01-01")
.end_date("2025-01-31")
.format(CostReportFormat::Csv)
.subscription_ids(vec![123, 456])
.regions(vec!["us-east-1".to_string()])
.tag("env", "prod")
.build()
.expect("should build with all required fields");
assert_eq!(request.start_date, "2025-01-01");
assert_eq!(request.end_date, "2025-01-31");
assert_eq!(request.format, Some(CostReportFormat::Csv));
assert_eq!(request.subscription_ids, Some(vec![123, 456]));
assert_eq!(request.regions, Some(vec!["us-east-1".to_string()]));
assert!(request.tags.is_some());
let tags = request.tags.unwrap();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].key, "env");
assert_eq!(tags[0].value, "prod");
}
#[test]
fn test_cost_report_request_simple() {
let request = CostReportCreateRequest::new("2025-01-01", "2025-01-31");
assert_eq!(request.start_date, "2025-01-01");
assert_eq!(request.end_date, "2025-01-31");
assert!(request.format.is_none());
}
#[test]
fn test_cost_report_format_display() {
assert_eq!(CostReportFormat::Csv.to_string(), "csv");
assert_eq!(CostReportFormat::Json.to_string(), "json");
}
#[test]
fn test_subscription_type_display() {
assert_eq!(SubscriptionType::Pro.to_string(), "pro");
assert_eq!(SubscriptionType::Essentials.to_string(), "essentials");
}
#[test]
fn test_tag_creation() {
let tag = Tag::new("environment", "production");
assert_eq!(tag.key, "environment");
assert_eq!(tag.value, "production");
}
#[test]
fn test_request_serialization() {
let request = CostReportCreateRequest::builder()
.start_date("2025-01-01")
.end_date("2025-01-31")
.format(CostReportFormat::Json)
.subscription_type(SubscriptionType::Pro)
.build()
.expect("should build with all required fields");
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["startDate"], "2025-01-01");
assert_eq!(json["endDate"], "2025-01-31");
assert_eq!(json["format"], "json");
assert_eq!(json["subscriptionType"], "pro");
}
#[test]
fn test_builder_missing_start_date() {
let result = CostReportCreateRequest::builder()
.end_date("2025-01-31")
.build();
assert!(result.is_err());
}
#[test]
fn test_builder_missing_end_date() {
let result = CostReportCreateRequest::builder()
.start_date("2025-01-01")
.build();
assert!(result.is_err());
}
}