use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use crate::errors::{SuperSTACError, ValidationError};
use crate::utils::{get_date_time, parse_url, validate_identifier};
#[derive(Clone, Debug, Serialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum HealthCheckFrequencyStrategy {
Minutely,
#[default]
Hourly,
Daily,
Weekly,
Monthly,
Custom(Duration),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CatalogSettings {
pub health_check_strategy: HealthCheckFrequencyStrategy,
pub healthy_status_code_range: (u16, u16),
}
impl Default for CatalogSettings {
fn default() -> Self {
CatalogSettings {
health_check_strategy: HealthCheckFrequencyStrategy::Hourly,
healthy_status_code_range: (200, 299),
}
}
}
impl HealthCheckFrequencyStrategy {
pub fn as_duration(&self) -> Duration {
match self {
HealthCheckFrequencyStrategy::Minutely => Duration::from_secs(60),
HealthCheckFrequencyStrategy::Hourly => Duration::from_secs(60 * 60),
HealthCheckFrequencyStrategy::Daily => Duration::from_secs(60 * 60 * 24),
HealthCheckFrequencyStrategy::Weekly => Duration::from_secs(60 * 60 * 24 * 7),
HealthCheckFrequencyStrategy::Monthly => Duration::from_secs(60 * 60 * 24 * 30),
HealthCheckFrequencyStrategy::Custom(dur) => *dur,
}
}
}
impl<'de> Deserialize<'de> for HealthCheckFrequencyStrategy {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = s.trim().to_lowercase();
match s.as_str() {
"minutely" => Ok(HealthCheckFrequencyStrategy::Minutely),
"hourly" => Ok(HealthCheckFrequencyStrategy::Hourly),
"daily" => Ok(HealthCheckFrequencyStrategy::Daily),
"weekly" => Ok(HealthCheckFrequencyStrategy::Weekly),
"monthly" => Ok(HealthCheckFrequencyStrategy::Monthly),
_ => {
let dur = if s.ends_with("s") {
let n = &s[..s.len() - 1]
.parse::<u64>()
.map_err(serde::de::Error::custom)?;
Duration::from_secs(*n)
} else if s.ends_with("m") {
let n = &s[..s.len() - 1]
.parse::<u64>()
.map_err(serde::de::Error::custom)?;
Duration::from_secs(*n * 60)
} else if s.ends_with("h") {
let n = &s[..s.len() - 1]
.parse::<u64>()
.map_err(serde::de::Error::custom)?;
Duration::from_secs(*n * 3600)
} else {
return Err(serde::de::Error::custom(format!("Invalid duration: {}", s)));
};
Ok(HealthCheckFrequencyStrategy::Custom(dur))
}
}
}
}
impl FromStr for HealthCheckFrequencyStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.ends_with("s") {
let secs = s[..s.len() - 1]
.parse::<u64>()
.map_err(|_| "Invalid seconds")?;
Ok(HealthCheckFrequencyStrategy::Custom(Duration::from_secs(
secs,
)))
} else if s.ends_with("m") {
let mins = s[..s.len() - 1]
.parse::<u64>()
.map_err(|_| "Invalid minutes")?;
Ok(HealthCheckFrequencyStrategy::Custom(Duration::from_secs(
mins * 60,
)))
} else if s.ends_with("h") {
let hours = s[..s.len() - 1]
.parse::<u64>()
.map_err(|_| "Invalid hours")?;
Ok(HealthCheckFrequencyStrategy::Custom(Duration::from_secs(
hours * 3600,
)))
} else {
Err(format!("Invalid custom duration: {}", s))
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct HealthStatus {
pub endpoint: String,
pub available: bool,
pub last_checked: Option<DateTime<Utc>>,
pub status_code: u16,
}
fn get_default_health_status(url: String) -> HealthStatus {
HealthStatus {
available: false,
endpoint: url,
last_checked: Some(get_date_time()),
status_code: 200,
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CatalogCapabilities {
filtering: String,
}
#[derive(Debug, Deserialize)]
pub struct CatalogConfig {
pub id: String,
pub provider: Option<String>,
pub title: Option<String>,
pub url: Option<String>,
pub description: Option<String>,
pub settings: Option<CatalogSettings>,
pub collection_aliases: Option<HashMap<String, String>>,
pub asset_aliases: Option<HashMap<String, HashMap<String, String>>>,
}
impl TryFrom<CatalogConfig> for Catalog {
type Error = SuperSTACError;
fn try_from(cfg: CatalogConfig) -> Result<Self, Self::Error> {
validate_identifier(&cfg.id)?;
let url = match cfg.url {
Some(w) => {
parse_url(&w).map_err(|e| ValidationError::InvalidUrl(e.to_string()))?;
Some(w)
}
None => None,
};
Ok(Self {
id: cfg.id,
provider: None,
title: cfg.title,
url: url.clone().unwrap(),
description: cfg.description,
settings: cfg.settings.unwrap_or(CatalogSettings::default()),
health_status: get_default_health_status(url.unwrap()),
capabilities: None,
collection_aliases: cfg.collection_aliases.unwrap_or_default(),
asset_aliases: cfg.asset_aliases.unwrap_or_default(),
supported_collections: None,
created_at: Some(get_date_time()),
updated_at: None,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Catalog {
pub id: String,
pub provider: Option<String>,
pub title: Option<String>,
pub url: String,
pub description: Option<String>,
pub settings: CatalogSettings,
pub health_status: HealthStatus,
pub capabilities: Option<CatalogCapabilities>,
#[serde(default)]
pub collection_aliases: HashMap<String, String>,
#[serde(default)]
pub asset_aliases: HashMap<String, HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_collections: Option<HashSet<String>>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
impl Catalog {
pub fn new(
id: &str,
title: Option<impl Into<String>>,
url: &str,
description: Option<impl Into<String>>,
settings: Option<CatalogSettings>,
) -> Result<Self, SuperSTACError> {
validate_identifier(&id)?;
let valid_url = parse_url(url)
.map_err(|err| SuperSTACError::from(ValidationError::InvalidUrl(err.to_string())))?;
let url_string = valid_url.to_string();
Ok(Self {
id: id.to_string(),
provider: None,
title: title.map(|t| t.into()),
url: url_string.clone(),
description: description.map(|d| d.into()),
settings: settings.unwrap_or_default(),
health_status: get_default_health_status(url_string),
capabilities: None,
collection_aliases: HashMap::new(),
asset_aliases: HashMap::new(),
supported_collections: None,
created_at: Some(get_date_time()),
updated_at: None,
})
}
pub fn with_collection_aliases(mut self, aliases: HashMap<String, String>) -> Self {
self.collection_aliases = aliases;
self
}
pub fn resolve_collection<'a>(&'a self, canonical: &'a str) -> &'a str {
self.collection_aliases
.get(canonical)
.map(String::as_str)
.unwrap_or(canonical)
}
pub fn canonical_collection<'a>(&'a self, local: &'a str) -> &'a str {
for (canonical, l) in &self.collection_aliases {
if l == local {
return canonical;
}
}
local
}
pub fn supports_any_of(&self, requested: &[String]) -> bool {
match &self.supported_collections {
None => true,
Some(set) => requested.is_empty() || requested.iter().any(|c| set.contains(c)),
}
}
pub fn set_update_date(&mut self) {
self.updated_at = Some(get_date_time());
}
pub fn update(
&mut self,
description: Option<String>,
url: Option<String>,
title: Option<String>,
settings: Option<CatalogSettings>,
) -> Result<(), ValidationError> {
if let Some(updated_url) = url {
self.url = parse_url(updated_url.as_str())
.map_err(|err| ValidationError::InvalidUrl(err.to_string()))?
.to_string();
}
self.title = title;
self.description = description;
if let Some(updated_settings) = settings {
self.settings = updated_settings;
}
self.set_update_date();
Ok(())
}
pub fn set_id(&mut self, id: String) {
self.id = id
}
pub fn set_provider(&mut self, provider: &str) {
self.provider = Some(provider.to_owned())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CatalogFilters {
pub id: Option<String>,
pub provider: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub available: Option<bool>,
pub created_after: Option<DateTime<Utc>>,
pub created_before: Option<DateTime<Utc>>,
pub updated_after: Option<DateTime<Utc>>,
pub updated_before: Option<DateTime<Utc>>,
}
impl Default for CatalogFilters {
fn default() -> Self {
CatalogFilters {
id: None,
provider: None,
title: None,
description: None,
available: None,
created_after: None,
created_before: None,
updated_after: None,
updated_before: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CatalogUpdate {
pub provider: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub url: Option<String>,
pub settings: Option<CatalogSettings>,
}