use std::collections::HashMap;
use chrono::{DateTime, Duration, Utc};
use serde_json;
use crate::client::CarbemClient;
use crate::error::{CarbemError, Result};
use crate::models::{CarbonEmission, EmissionQuery, TimePeriod};
use crate::providers::azure::AzureConfig;
use crate::providers::config::ProviderQueryConfig;
use crate::providers::ibm::IbmConfig;
pub async fn get_emissions(
provider: &str,
json_config: &str,
json_payload: &str,
) -> Result<Vec<CarbonEmission>> {
let client = create_client_from_json(provider, json_config)?;
let query = parse_emission_query_from_json(provider, json_payload)?;
client.query_emissions(&query).await
}
fn create_client_from_json(provider: &str, json_config: &str) -> Result<CarbemClient> {
match provider {
"azure" => {
let config: AzureConfig =
serde_json::from_str(json_config).map_err(CarbemError::Json)?;
let client = CarbemClient::builder().with_azure(config)?.build();
Ok(client)
}
"ibm" => {
let config: IbmConfig = serde_json::from_str(json_config).map_err(CarbemError::Json)?;
let client = CarbemClient::builder().with_ibm(config)?.build();
Ok(client)
}
_ => Err(CarbemError::UnsupportedProvider(provider.to_string())),
}
}
fn parse_emission_query_from_json(provider: &str, json_payload: &str) -> Result<EmissionQuery> {
let payload: HashMap<String, serde_json::Value> =
serde_json::from_str(json_payload).map_err(CarbemError::Json)?;
let start_date = match payload.get("start_date") {
Some(value) => match value.as_str() {
Some(date_str) => DateTime::parse_from_rfc3339(date_str)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|_| {
CarbemError::Config(format!(
"Invalid start_date format '{}': expected RFC3339 format (e.g., '2024-01-01T00:00:00Z')",
date_str
))
})?,
None => {
return Err(CarbemError::Config(
"start_date must be a string".to_string(),
))
}
},
None => Utc::now() - Duration::days(30), };
let end_date = match payload.get("end_date") {
Some(value) => match value.as_str() {
Some(date_str) => DateTime::parse_from_rfc3339(date_str)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|_| {
CarbemError::Config(format!(
"Invalid end_date format '{}': expected RFC3339 format (e.g., '2024-01-01T00:00:00Z')",
date_str
))
})?,
None => {
return Err(CarbemError::Config(
"end_date must be a string".to_string(),
))
}
},
None => Utc::now(), };
let regions = payload
.get("regions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let services = payload
.get("services")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
});
let resources = payload
.get("resources")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
});
let provider_config = match provider {
"azure" => {
use crate::providers::azure::AzureQueryConfig;
let config = serde_json::from_value::<AzureQueryConfig>(
serde_json::to_value(&payload).map_err(CarbemError::Json)?,
)
.map_err(|e| {
CarbemError::Config(format!(
"Failed to parse Azure provider configuration: {}.",
e
))
})?;
Some(ProviderQueryConfig::Azure(config))
}
"ibm" => {
use crate::providers::ibm::IbmQueryConfig;
let config = serde_json::from_value::<IbmQueryConfig>(
serde_json::to_value(&payload).map_err(CarbemError::Json)?,
)
.map_err(|e| {
CarbemError::Config(format!(
"Failed to parse IBM provider configuration: {}.",
e
))
})?;
Some(ProviderQueryConfig::Ibm(config))
}
_ => None,
};
Ok(EmissionQuery {
provider: provider.to_string(),
time_period: TimePeriod {
start: start_date,
end: end_date,
},
regions,
services,
resources,
provider_config,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_emission_query_from_json() {
let json = r#"{
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-02-01T00:00:00Z",
"regions": ["eastus", "westus"],
"services": ["compute", "storage"],
"report_type": "MonthlySummaryReport",
"subscription_list": ["sub-1", "sub-2"]
}"#;
let query = parse_emission_query_from_json("azure", json).unwrap();
assert_eq!(query.provider, "azure");
assert_eq!(query.regions, vec!["eastus", "westus"]);
assert_eq!(
query.services,
Some(vec!["compute".to_string(), "storage".to_string()])
);
assert_eq!(query.resources, None);
}
#[test]
fn test_parse_emission_query_invalid_dates() {
let json_invalid_start = r#"{
"start_date": "invalid-date",
"end_date": "2024-02-01T00:00:00Z",
"regions": ["eastus"],
"report_type": "MonthlySummaryReport",
"subscription_list": ["sub-1"]
}"#;
let result = parse_emission_query_from_json("azure", json_invalid_start);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid start_date format")
);
let json_invalid_end = r#"{
"start_date": "2024-01-01T00:00:00Z",
"end_date": "not-a-date",
"regions": ["eastus"],
"report_type": "MonthlySummaryReport",
"subscription_list": ["sub-1"]
}"#;
let result = parse_emission_query_from_json("azure", json_invalid_end);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid end_date format")
);
let json_no_dates = r#"{
"regions": ["eastus"],
"report_type": "MonthlySummaryReport",
"subscription_list": ["sub-1"]
}"#;
let result = parse_emission_query_from_json("azure", json_no_dates);
assert!(result.is_ok());
let query = result.unwrap();
assert_eq!(query.regions, vec!["eastus"]);
}
#[tokio::test]
#[ignore] async fn test_get_emissions_integration() {
if let Ok(token) = std::env::var("AZURE_TOKEN") {
let config = format!(r#"{{"access_token": "{}"}}"#, token);
let payload = r#"{
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-02-01T00:00:00Z",
"regions": ["your-subscription-id"]
}"#;
let result = get_emissions("azure", &config, payload).await;
assert!(result.is_ok());
}
}
}