use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_as_string<'de, D>(d: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{Error, Visitor};
struct V;
impl<'de> Visitor<'de> for V {
type Value = String;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a string or number")
}
fn visit_str<E: Error>(self, v: &str) -> Result<String, E> {
Ok(v.to_owned())
}
fn visit_string<E: Error>(self, v: String) -> Result<String, E> {
Ok(v)
}
fn visit_f64<E: Error>(self, v: f64) -> Result<String, E> {
Ok(v.to_string())
}
fn visit_i64<E: Error>(self, v: i64) -> Result<String, E> {
Ok(v.to_string())
}
fn visit_u64<E: Error>(self, v: u64) -> Result<String, E> {
Ok(v.to_string())
}
}
d.deserialize_any(V)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{AppNetwork, NetworkCounter, RuntimeStats};
#[test]
fn network_counter_formatted_variant() {
let v: NetworkCounter = serde_json::from_value(json!("1 MB")).unwrap();
assert!(matches!(v, NetworkCounter::Formatted(_)));
}
#[test]
fn network_counter_raw_variant() {
let v: NetworkCounter =
serde_json::from_value(json!([1000, 2000])).unwrap();
assert!(matches!(v, NetworkCounter::Raw(_)));
}
#[test]
fn deserialize_as_string_accepts_string() {
let stats: RuntimeStats = serde_json::from_value(json!({
"cpu": "3.2%", "ram": "128/512MB", "status": "running",
"running": true, "storage": "50MB",
"network": { "total": "1MB", "now": "0KB" },
"uptime": null
}))
.unwrap();
assert_eq!(stats.cpu, "3.2%");
}
#[test]
fn deserialize_as_string_accepts_number() {
let stats: RuntimeStats = serde_json::from_value(json!({
"cpu": 3.2, "ram": 128, "status": "running",
"running": true, "storage": 52428800,
"network": { "total": [1000, 2000], "now": [0, 0] },
"uptime": null
}))
.unwrap();
assert_eq!(stats.cpu, "3.2");
assert_eq!(stats.ram, "128");
}
#[test]
fn app_network_accepts_mixed_counter_types() {
let n: AppNetwork =
serde_json::from_value(json!({ "total": "1 MB", "now": [0, 0] }))
.unwrap();
assert!(matches!(n.total, NetworkCounter::Formatted(_)));
assert!(matches!(n.now, NetworkCounter::Raw(_)));
}
#[test]
fn deserialize_as_string_accepts_negative_integer() {
let stats: RuntimeStats = serde_json::from_value(json!({
"cpu": -1, "ram": -256, "status": "exited",
"running": false, "storage": -0,
"network": { "total": "0KB", "now": "0KB" },
"uptime": null
}))
.unwrap();
assert_eq!(stats.cpu, "-1");
assert_eq!(stats.ram, "-256");
}
#[test]
fn deserialize_as_string_rejects_invalid_type() {
let result: Result<RuntimeStats, _> = serde_json::from_value(json!({
"cpu": true, "ram": "128/512MB", "status": "running",
"running": true, "storage": "50MB",
"network": { "total": "1MB", "now": "0KB" },
"uptime": null
}));
assert!(result.is_err());
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum NetworkCounter {
Formatted(String),
Raw(Vec<u64>),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppNetwork {
pub total: NetworkCounter,
pub now: NetworkCounter,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeStatsListItem {
pub id: String,
pub running: bool,
pub cpu: Option<String>,
pub ram: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeStats {
#[serde(deserialize_with = "deserialize_as_string")]
pub cpu: String,
#[serde(deserialize_with = "deserialize_as_string")]
pub ram: String,
pub status: String,
pub running: bool,
#[serde(deserialize_with = "deserialize_as_string")]
pub storage: String,
pub network: AppNetwork,
#[serde(with = "chrono::serde::ts_milliseconds_option")]
pub uptime: Option<DateTime<Utc>>,
}