use std::collections::HashMap;
use std::io::Write;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::cli::drive::format::{write_scalar_jsonl, JsonlSerialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Owner {
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "displayName"
)]
pub display_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "emailAddress"
)]
pub email_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct DriveFile {
pub id: String,
pub name: String,
#[serde(default, rename = "mimeType")]
pub mime_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "md5Checksum"
)]
pub md5_checksum: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "sha1Checksum"
)]
pub sha1_checksum: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "sha256Checksum"
)]
pub sha256_checksum: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "modifiedTime"
)]
pub modified_time: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parents: Vec<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "webViewLink"
)]
pub web_view_link: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub owners: Vec<Owner>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "driveId")]
pub drive_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "exportLinks"
)]
pub export_links: Option<HashMap<String, String>>,
}
impl DriveFile {
#[must_use]
pub fn is_google_native(&self) -> bool {
self.mime_type.starts_with("application/vnd.google-apps.")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileListResponse {
#[serde(default)]
pub files: Vec<DriveFile>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "nextPageToken"
)]
pub next_page_token: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "incompleteSearch"
)]
pub incomplete_search: Option<bool>,
}
impl JsonlSerialize for DriveFile {
fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
write_scalar_jsonl(self, out)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct DrivePermission {
#[serde(default)]
pub id: String,
#[serde(default, rename = "type")]
pub permission_type: String,
#[serde(default)]
pub role: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "emailAddress"
)]
pub email_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn file_list_response_deserializes_a_realistic_fixture() {
let json = serde_json::json!({
"files": [
{
"id": "f1",
"name": "report.pdf",
"mimeType": "application/pdf",
"size": "12345",
"md5Checksum": "5d41402abc4b2a76b9719d911017c592",
"sha1Checksum": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
"sha256Checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"modifiedTime": "2026-01-01T00:00:00.000Z",
"parents": ["folder1"],
"webViewLink": "https://drive.google.com/file/d/f1/view",
"owners": [{"displayName": "Alice", "emailAddress": "alice@example.com"}],
"driveId": "shared1",
},
],
"nextPageToken": "page2",
"incompleteSearch": false,
});
let response: FileListResponse = serde_json::from_value(json).unwrap();
assert_eq!(response.files.len(), 1);
let file = &response.files[0];
assert_eq!(file.id, "f1");
assert_eq!(file.size.as_deref(), Some("12345"));
assert_eq!(
file.md5_checksum.as_deref(),
Some("5d41402abc4b2a76b9719d911017c592")
);
assert_eq!(
file.sha1_checksum.as_deref(),
Some("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")
);
assert_eq!(
file.sha256_checksum.as_deref(),
Some("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")
);
assert_eq!(
file.owners[0].email_address.as_deref(),
Some("alice@example.com")
);
assert_eq!(file.drive_id.as_deref(), Some("shared1"));
assert_eq!(response.next_page_token.as_deref(), Some("page2"));
}
#[test]
fn checksums_are_none_when_absent() {
let json = serde_json::json!({"id": "f1", "name": "n"});
let file: DriveFile = serde_json::from_value(json).unwrap();
assert!(file.md5_checksum.is_none());
assert!(file.sha1_checksum.is_none());
assert!(file.sha256_checksum.is_none());
}
#[test]
fn size_round_trips_as_a_string_not_a_number() {
let file = DriveFile {
id: "f1".to_string(),
name: "n".to_string(),
size: Some("999".to_string()),
..Default::default()
};
let value = serde_json::to_value(&file).unwrap();
assert_eq!(value["size"], serde_json::json!("999"));
}
#[test]
fn is_google_native_true_for_google_apps_mime_type() {
let file = DriveFile {
mime_type: "application/vnd.google-apps.document".to_string(),
..Default::default()
};
assert!(file.is_google_native());
}
#[test]
fn is_google_native_false_for_ordinary_mime_type() {
let file = DriveFile {
mime_type: "application/pdf".to_string(),
..Default::default()
};
assert!(!file.is_google_native());
}
#[test]
fn drive_file_deserializes_from_minimal_fields() {
let json = serde_json::json!({"id": "f1", "name": "n"});
let file: DriveFile = serde_json::from_value(json).unwrap();
assert_eq!(file.id, "f1");
assert_eq!(file.mime_type, "");
assert!(file.size.is_none());
assert!(file.export_links.is_none());
}
#[test]
fn deserializing_unmodeled_extra_field_still_succeeds() {
let json = serde_json::json!({
"id": "f1",
"name": "n",
"somethingNew": {"nested": true},
});
let file: DriveFile = serde_json::from_value(json).unwrap();
assert_eq!(file.id, "f1");
}
#[test]
fn export_links_parses_google_native_export_map() {
let json = serde_json::json!({
"id": "f1",
"name": "doc",
"mimeType": "application/vnd.google-apps.document",
"exportLinks": {
"text/markdown": "https://export.example/md",
"application/pdf": "https://export.example/pdf",
},
});
let file: DriveFile = serde_json::from_value(json).unwrap();
let links = file.export_links.unwrap();
assert_eq!(links.len(), 2);
assert!(links.contains_key("text/markdown"));
}
#[test]
fn drive_file_write_jsonl_emits_exactly_one_line() {
let file = DriveFile {
id: "f1".to_string(),
name: "n".to_string(),
..Default::default()
};
let mut buf = Vec::new();
file.write_jsonl(&mut buf).unwrap();
let text = String::from_utf8(buf).unwrap();
assert_eq!(text.lines().count(), 1);
assert!(text.contains("f1"));
}
}