use serde::{Deserialize, Serialize}; /// Whether an asset is an image or a video. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AssetItemType { Image, Movie, } /// Version size key for asset versions. /// /// Uses `#[repr(u8)]` to guarantee 1-byte size for better struct packing. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] pub enum AssetVersionSize { Original = 0, Alternative = 1, Medium = 2, Thumb = 3, Adjusted = 4, LiveOriginal = 5, LiveMedium = 6, LiveThumb = 7, LiveAdjusted = 8, } impl From for AssetVersionSize { fn from(v: VersionSize) -> Self { match v { VersionSize::Original => Self::Original, VersionSize::Medium => Self::Medium, VersionSize::Thumb => Self::Thumb, VersionSize::Adjusted => Self::Adjusted, VersionSize::Alternative => Self::Alternative, } } } /// Reason for a record change in a delta/incremental sync response. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChangeReason { /// New or modified record (state DB determines which). Created, /// Soft-deleted (moved to trash / recently deleted). SoftDeleted, /// Permanently purged from the source. HardDeleted, /// Hidden from the main library view. Hidden, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum VersionSize { Original, Medium, Thumb, Adjusted, Alternative, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum LivePhotoSize { Original, Medium, Thumb, Adjusted, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] #[serde(rename_all = "lowercase")] pub enum PhotoResolution { None = 0, Original = 1, Medium = 2, Thumb = 3, } impl PhotoResolution { pub const fn to_asset_version_size(self) -> Option { match self { Self::None => None, Self::Original => Some(AssetVersionSize::Original), Self::Medium => Some(AssetVersionSize::Medium), Self::Thumb => Some(AssetVersionSize::Thumb), } } } impl From for PhotoResolution { fn from(value: VersionSize) -> Self { match value { VersionSize::Original | VersionSize::Adjusted | VersionSize::Alternative => { Self::Original } VersionSize::Medium => Self::Medium, VersionSize::Thumb => Self::Thumb, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] #[serde(rename_all = "lowercase")] pub enum LivePhotoResolution { Original = 0, Medium = 1, Thumb = 2, } impl LivePhotoResolution { pub const fn to_asset_version_size(self) -> AssetVersionSize { match self { Self::Original => AssetVersionSize::LiveOriginal, Self::Medium => AssetVersionSize::LiveMedium, Self::Thumb => AssetVersionSize::LiveThumb, } } } impl From for LivePhotoResolution { fn from(value: LivePhotoSize) -> Self { match value.to_asset_version_size() { AssetVersionSize::LiveOriginal | AssetVersionSize::LiveAdjusted => Self::Original, AssetVersionSize::LiveMedium => Self::Medium, AssetVersionSize::LiveThumb => Self::Thumb, _ => Self::Original, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Domain { Com, Cn, } impl Domain { pub const fn as_str(&self) -> &str { match self { Self::Com => "com", Self::Cn => "cn", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum LogLevel { Debug, Info, Warn, Error, } #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] pub enum FileMatchPolicy { #[value(name = "name-size-dedup-with-suffix")] #[serde(rename = "name-size-dedup-with-suffix")] NameSizeDedupWithSuffix = 0, #[value(name = "name-id7")] #[serde(rename = "name-id7")] NameId7 = 1, } #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] pub enum RawTreatmentPolicy { #[value(name = "as-is")] #[serde(rename = "as-is")] Unchanged = 0, #[value(name = "original")] #[serde(rename = "original")] PreferOriginal = 1, #[value(name = "alternative")] #[serde(rename = "alternative")] PreferAlternative = 2, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] pub enum RawPolicy { #[value(name = "as-is")] #[serde(rename = "as-is")] AsIs = 0, #[value(name = "prefer-raw")] #[serde(rename = "prefer-raw")] PreferRaw = 1, #[value(name = "prefer-jpeg")] #[serde(rename = "prefer-jpeg")] PreferJpeg = 2, } impl From for RawPolicy { fn from(value: RawTreatmentPolicy) -> Self { match value { RawTreatmentPolicy::Unchanged => Self::AsIs, RawTreatmentPolicy::PreferOriginal => Self::PreferRaw, RawTreatmentPolicy::PreferAlternative => Self::PreferJpeg, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] #[serde(rename_all = "lowercase")] pub enum LivePhotoMovFilenamePolicy { Suffix = 0, Original = 1, } /// Controls which components of live photos are downloaded. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] #[repr(u8)] #[serde(rename_all = "kebab-case")] pub enum LivePhotoMode { /// Download both the still image and the MOV video #[default] Both, /// Download only the still image, skip the MOV #[value(name = "image-only")] ImageOnly, /// Download only the MOV video, skip the still image #[value(name = "video-only")] VideoOnly, /// Skip live photos entirely (both image and MOV) Skip, } impl LivePhotoSize { pub fn to_asset_version_size(self) -> AssetVersionSize { match self { Self::Original => AssetVersionSize::LiveOriginal, Self::Medium => AssetVersionSize::LiveMedium, Self::Thumb => AssetVersionSize::LiveThumb, Self::Adjusted => AssetVersionSize::LiveAdjusted, } } } #[cfg(test)] mod tests { use super::*; use serde::de::DeserializeOwned; use std::fmt::Debug; #[test] fn test_live_photo_size_to_asset_version_size() { assert_eq!( LivePhotoSize::Original.to_asset_version_size(), AssetVersionSize::LiveOriginal ); assert_eq!( LivePhotoSize::Medium.to_asset_version_size(), AssetVersionSize::LiveMedium ); assert_eq!( LivePhotoSize::Thumb.to_asset_version_size(), AssetVersionSize::LiveThumb ); assert_eq!( LivePhotoSize::Adjusted.to_asset_version_size(), AssetVersionSize::LiveAdjusted ); } #[test] fn test_domain_as_str() { assert_eq!(Domain::Com.as_str(), "com"); assert_eq!(Domain::Cn.as_str(), "cn"); } #[test] fn version_size_serde_round_trip() { for (variant, expected) in [ (VersionSize::Original, "\"original\""), (VersionSize::Medium, "\"medium\""), (VersionSize::Thumb, "\"thumb\""), (VersionSize::Adjusted, "\"adjusted\""), (VersionSize::Alternative, "\"alternative\""), ] { let json = serde_json::to_string(&variant).expect("serialize VersionSize"); assert_eq!(json, expected); let parsed: VersionSize = serde_json::from_str(&json).expect("deserialize VersionSize"); assert_eq!(parsed, variant); } } #[test] fn live_photo_size_serde_round_trip() { for (variant, expected) in [ (LivePhotoSize::Original, "\"original\""), (LivePhotoSize::Medium, "\"medium\""), (LivePhotoSize::Thumb, "\"thumb\""), (LivePhotoSize::Adjusted, "\"adjusted\""), ] { let json = serde_json::to_string(&variant).expect("serialize LivePhotoSize"); assert_eq!(json, expected); let parsed: LivePhotoSize = serde_json::from_str(&json).expect("deserialize LivePhotoSize"); assert_eq!(parsed, variant); } } #[test] fn domain_serde_round_trip() { for (variant, expected) in [(Domain::Com, "\"com\""), (Domain::Cn, "\"cn\"")] { let json = serde_json::to_string(&variant).expect("serialize Domain"); assert_eq!(json, expected); let parsed: Domain = serde_json::from_str(&json).expect("deserialize Domain"); assert_eq!(parsed, variant); } } #[test] fn log_level_serde_round_trip() { for (variant, expected) in [ (LogLevel::Debug, "\"debug\""), (LogLevel::Info, "\"info\""), (LogLevel::Warn, "\"warn\""), (LogLevel::Error, "\"error\""), ] { let json = serde_json::to_string(&variant).expect("serialize LogLevel"); assert_eq!(json, expected); let parsed: LogLevel = serde_json::from_str(&json).expect("deserialize LogLevel"); assert_eq!(parsed, variant); } } #[test] fn file_match_policy_serde_round_trip() { for (variant, expected) in [ ( FileMatchPolicy::NameSizeDedupWithSuffix, "\"name-size-dedup-with-suffix\"", ), (FileMatchPolicy::NameId7, "\"name-id7\""), ] { let json = serde_json::to_string(&variant).expect("serialize FileMatchPolicy"); assert_eq!(json, expected); let parsed: FileMatchPolicy = serde_json::from_str(&json).expect("deserialize FileMatchPolicy"); assert_eq!(parsed, variant); } } #[test] fn raw_treatment_policy_serde_round_trip() { for (variant, expected) in [ (RawTreatmentPolicy::Unchanged, "\"as-is\""), (RawTreatmentPolicy::PreferOriginal, "\"original\""), (RawTreatmentPolicy::PreferAlternative, "\"alternative\""), ] { let json = serde_json::to_string(&variant).expect("serialize RawTreatmentPolicy"); assert_eq!(json, expected); let parsed: RawTreatmentPolicy = serde_json::from_str(&json).expect("deserialize RawTreatmentPolicy"); assert_eq!(parsed, variant); } } #[test] fn live_photo_mode_serde_round_trip() { for (variant, expected) in [ (LivePhotoMode::Both, "\"both\""), (LivePhotoMode::ImageOnly, "\"image-only\""), (LivePhotoMode::VideoOnly, "\"video-only\""), (LivePhotoMode::Skip, "\"skip\""), ] { let json = serde_json::to_string(&variant).expect("serialize LivePhotoMode"); assert_eq!(json, expected); let parsed: LivePhotoMode = serde_json::from_str(&json).expect("deserialize LivePhotoMode"); assert_eq!(parsed, variant); } } #[test] fn live_photo_mov_filename_policy_serde_round_trip() { for (variant, expected) in [ (LivePhotoMovFilenamePolicy::Suffix, "\"suffix\""), (LivePhotoMovFilenamePolicy::Original, "\"original\""), ] { let json = serde_json::to_string(&variant).expect("serialize LivePhotoMovFilenamePolicy"); assert_eq!(json, expected); let parsed: LivePhotoMovFilenamePolicy = serde_json::from_str(&json).expect("deserialize LivePhotoMovFilenamePolicy"); assert_eq!(parsed, variant); } } fn assert_serde_string_rejected(value: &str) where T: Debug + DeserializeOwned, { let json = serde_json::to_string(value).expect("serialize test string"); serde_json::from_str::(&json).expect_err("invalid enum value should be rejected"); } #[test] fn durable_config_enums_reject_unknown_serde_strings() { assert_serde_string_rejected::("full"); assert_serde_string_rejected::("live-original"); assert_serde_string_rejected::("adjusted"); assert_serde_string_rejected::("adjusted"); assert_serde_string_rejected::("icloud.com"); assert_serde_string_rejected::("warning"); assert_serde_string_rejected::("name-size"); assert_serde_string_rejected::("prefer-raw"); assert_serde_string_rejected::("original"); assert_serde_string_rejected::("mov"); assert_serde_string_rejected::("video_only"); } #[test] fn from_version_size_all_variants() { for (input, expected) in [ (VersionSize::Original, AssetVersionSize::Original), (VersionSize::Medium, AssetVersionSize::Medium), (VersionSize::Thumb, AssetVersionSize::Thumb), (VersionSize::Adjusted, AssetVersionSize::Adjusted), (VersionSize::Alternative, AssetVersionSize::Alternative), ] { assert_eq!(AssetVersionSize::from(input), expected, "from {input:?}"); } } #[test] fn asset_version_size_is_one_byte() { assert_eq!(std::mem::size_of::(), 1); } #[test] fn asset_version_size_variants_have_distinct_repr_values() { let variants = [ AssetVersionSize::Original as u8, AssetVersionSize::Alternative as u8, AssetVersionSize::Medium as u8, AssetVersionSize::Thumb as u8, AssetVersionSize::Adjusted as u8, AssetVersionSize::LiveOriginal as u8, AssetVersionSize::LiveMedium as u8, AssetVersionSize::LiveThumb as u8, AssetVersionSize::LiveAdjusted as u8, ]; let unique: std::collections::HashSet = variants.iter().copied().collect(); assert_eq!( unique.len(), variants.len(), "all repr(u8) values must be distinct" ); } } /// Placement of selected adjusted renditions. Primary mode opts into /// preservation-backed management of kei-owned current paths. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum EditedNaming { #[default] Suffix, Primary, }