use std::time::Duration;
use serde::{Deserialize, Serialize};
use thisversion::traits::{IntoModel, IntoSchema};
use thisversion::{VersionBoundary, VersionFamily};
#[derive(Deserialize, Serialize)]
struct SettingsV0 {
#[serde(alias = "label", alias = "title")]
name: String,
#[serde(default)]
retries: Option<u8>,
#[serde(default)]
timeout_secs: Option<u32>,
#[serde(default)]
timeout_ms: Option<u64>,
#[serde(alias = "url")]
endpoint: String,
}
impl From<SettingsV0> for SettingsV1 {
fn from(previous: SettingsV0) -> Self {
fn normalize_endpoint(endpoint: String) -> String {
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint
} else {
format!("https://{endpoint}")
}
}
let timeout = match (previous.timeout_secs, previous.timeout_ms) {
(Some(secs), None) => Duration::from_secs(u64::from(secs)),
(None, Some(millis)) => Duration::from_millis(millis),
(None, None) => Duration::from_secs(30),
(Some(_), Some(_)) => {
unreachable!("legacy settings never contained both timeout fields")
}
};
Self {
name: previous.name,
retries: previous.retries.unwrap_or(0),
timeout,
endpoint: normalize_endpoint(previous.endpoint),
}
}
}
#[derive(Deserialize, Serialize, VersionFamily)]
#[serde(tag = "version", content = "data")]
enum SettingsVersions {
V0(SettingsV0),
V1(SettingsV1),
}
#[derive(Deserialize, Serialize, VersionBoundary)]
#[thisversion(model = Settings)]
struct SettingsV1 {
name: String,
retries: u8,
#[serde(rename = "timeout_ms", with = "duration_millis")]
timeout: Duration,
endpoint: String,
}
mod duration_millis {
use super::Duration;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let millis = u64::try_from(duration.as_millis()).map_err(serde::ser::Error::custom)?;
serializer.serialize_u64(millis)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
u64::deserialize(deserializer).map(Duration::from_millis)
}
}
#[derive(Debug, PartialEq, Eq)]
struct Settings {
name: String,
retries: u8,
timeout: Duration,
endpoint: String,
}
fn main() {
let settings = SettingsVersions::V0(
serde_json::from_str::<SettingsV0>(
r#"{
"label": "worker",
"timeout_secs": 15,
"endpoint": "api.example.com"
}"#,
)
.unwrap(),
)
.into_model();
assert_eq!(
settings,
Settings {
name: "worker".to_owned(),
retries: 0,
timeout: Duration::from_secs(15),
endpoint: "https://api.example.com".to_owned(),
}
);
let persisted: SettingsVersions = settings.into_schema();
let persisted = serde_json::to_string(&persisted).unwrap();
assert!(persisted.contains(r#""version":"V1""#));
assert!(persisted.contains(r#""timeout_ms":15000"#));
}