use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "lowercase")]
pub enum AutoSyncPolicy {
Manual,
#[default]
Books,
All,
}
impl AutoSyncPolicy {
pub fn as_str(self) -> &'static str {
match self {
AutoSyncPolicy::Manual => "manual",
AutoSyncPolicy::Books => "books",
AutoSyncPolicy::All => "all",
}
}
pub fn from_token_or_default(s: &str) -> AutoSyncPolicy {
s.parse().unwrap_or_default()
}
}
impl fmt::Display for AutoSyncPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for AutoSyncPolicy {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"all" | "everything" | "full" => Ok(AutoSyncPolicy::All),
"books" | "book" | "true" => Ok(AutoSyncPolicy::Books),
"manual" | "optin" | "opt-in" | "off" | "false" | "none" => {
Ok(AutoSyncPolicy::Manual)
}
_ => Err(()),
}
}
}
pub fn should_auto_sync(
policy: AutoSyncPolicy,
opted_in: bool,
looks_like_book: bool,
) -> bool {
match policy {
AutoSyncPolicy::All => true,
AutoSyncPolicy::Books => opted_in || looks_like_book,
AutoSyncPolicy::Manual => opted_in,
}
}
pub const BOOK_MIN_PDF_PAGES: u32 = 40;
pub const BOOK_MIN_TEXT_LINES: usize = 1500;
const EBOOK_FORMATS: &[&str] = &["epub", "mobi", "azw", "azw3", "fb2", "kepub"];
pub fn looks_like_book(
format: &str,
total_lines: usize,
page_count: Option<u32>,
) -> bool {
let fmt = format.trim().to_ascii_lowercase();
if EBOOK_FORMATS.contains(&fmt.as_str()) {
return true;
}
if let Some(pages) = page_count {
return pages >= BOOK_MIN_PDF_PAGES;
}
total_lines >= BOOK_MIN_TEXT_LINES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_books() {
assert_eq!(AutoSyncPolicy::default(), AutoSyncPolicy::Books);
}
#[test]
fn token_round_trips() {
for p in
[AutoSyncPolicy::Manual, AutoSyncPolicy::Books, AutoSyncPolicy::All]
{
assert_eq!(p.as_str().parse::<AutoSyncPolicy>(), Ok(p));
}
}
#[test]
fn legacy_bool_tokens_migrate() {
assert_eq!("true".parse(), Ok(AutoSyncPolicy::Books));
assert_eq!("false".parse(), Ok(AutoSyncPolicy::Manual));
assert_eq!(" OFF ".parse(), Ok(AutoSyncPolicy::Manual));
assert_eq!(
AutoSyncPolicy::from_token_or_default("nonsense"),
AutoSyncPolicy::Books
);
}
#[test]
fn serde_uses_lowercase_tokens() {
assert_eq!(
serde_json::to_string(&AutoSyncPolicy::Books).unwrap(),
"\"books\""
);
let back: AutoSyncPolicy = serde_json::from_str("\"manual\"").unwrap();
assert_eq!(back, AutoSyncPolicy::Manual);
}
#[test]
fn should_auto_sync_matrix() {
use AutoSyncPolicy::*;
assert!(should_auto_sync(All, false, false));
assert!(should_auto_sync(Books, false, true));
assert!(should_auto_sync(Books, true, false));
assert!(!should_auto_sync(Books, false, false));
assert!(should_auto_sync(Manual, true, false));
assert!(!should_auto_sync(Manual, false, true));
}
#[test]
fn ebook_formats_are_always_books() {
for fmt in ["epub", "MOBI", "azw3", "fb2", " kepub "] {
assert!(looks_like_book(fmt, 0, None), "{fmt} should be a book");
}
}
#[test]
fn pdf_uses_page_count_when_known() {
assert!(looks_like_book("pdf", 10, Some(BOOK_MIN_PDF_PAGES)));
assert!(looks_like_book("pdf", 10, Some(500)));
assert!(!looks_like_book("pdf", 100_000, Some(8)));
}
#[test]
fn text_uses_line_threshold_when_unpaginated() {
assert!(looks_like_book("txt", BOOK_MIN_TEXT_LINES, None));
assert!(looks_like_book("text", 20_000, None));
assert!(!looks_like_book("txt", 200, None));
assert!(!looks_like_book("md", 300, None));
assert!(looks_like_book("pdf", 5_000, None));
assert!(!looks_like_book("pdf", 300, None));
}
}