use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_ms_option<'de, D>(
d: D,
) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{Error, Visitor};
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<DateTime<Utc>>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a unix timestamp in milliseconds or null")
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(Utc.timestamp_millis_opt(v).single())
}
fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(Utc.timestamp_millis_opt(v as i64).single())
}
fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(Utc.timestamp_millis_opt(v as i64).single())
}
fn visit_some<D2: Deserializer<'de>>(
self,
d2: D2,
) -> Result<Self::Value, D2::Error> {
d2.deserialize_any(V)
}
}
d.deserialize_option(V)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{FileInfo, FileType};
#[test]
fn file_type_deserializes_file_and_directory() {
let f: FileType = serde_json::from_str(r#""file""#).unwrap();
assert!(matches!(f, FileType::File));
let d: FileType = serde_json::from_str(r#""directory""#).unwrap();
assert!(matches!(d, FileType::Directory));
}
fn parse(json: serde_json::Value) -> FileInfo {
serde_json::from_value(json).unwrap()
}
#[test]
fn last_modified_null_becomes_none() {
let fi = parse(json!({
"name": "main.py", "type": "file", "size": 0,
"lastModified": null
}));
assert!(fi.last_modified.is_none());
}
#[test]
fn last_modified_absent_becomes_none() {
let fi = parse(json!({ "name": "main.py", "type": "file" }));
assert!(fi.last_modified.is_none());
}
#[test]
fn last_modified_integer_ms() {
let fi = parse(json!({
"name": "f", "type": "file",
"lastModified": 1_700_000_000_000_i64
}));
assert!(fi.last_modified.is_some());
}
#[test]
fn last_modified_float_ms() {
let fi = parse(json!({
"name": "f", "type": "file",
"lastModified": 1_783_269_964_121.6409_f64
}));
assert!(fi.last_modified.is_some());
}
#[test]
fn last_modified_negative_ms_is_pre_epoch() {
let fi = parse(json!({
"name": "f", "type": "file",
"lastModified": -1000_i64
}));
assert!(fi.last_modified.is_some());
}
#[test]
fn last_modified_wrong_type_returns_error() {
let result: Result<FileInfo, _> = serde_json::from_value(json!({
"name": "f", "type": "file",
"lastModified": "not-a-timestamp"
}));
assert!(result.is_err());
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileContent {
#[serde(rename = "type")]
pub data_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileType {
File,
Directory,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileInfo {
pub name: String,
#[serde(rename = "type")]
pub file_type: FileType,
#[serde(default)]
pub size: u64,
#[serde(
deserialize_with = "deserialize_ms_option",
rename = "lastModified",
default
)]
pub last_modified: Option<DateTime<Utc>>,
}