From 4c2c65bd5a0ba34bc0ef4e86719c01459c143370 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Tue, 16 Dec 2025 11:11:36 +0100 Subject: [PATCH 01/12] Update tools_private submodule --- tools_private | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools_private b/tools_private index 7f8e961..1b824b1 160000 --- a/tools_private +++ b/tools_private @@ -1 +1 @@ -Subproject commit 7f8e9613c5fd15fb70290274731f58d7228197bc +Subproject commit 1b824b15c074e128a8708b86244cc045f4a2d782 From 1e57226b9933319e2e61749b1e317a753c89cf78 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 17:55:22 +0200 Subject: [PATCH 02/12] chore: rename submodule path --- .gitmodules | 4 ++-- tools_private => dev_tools | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename tools_private => dev_tools (100%) diff --git a/.gitmodules b/.gitmodules index ea567e2..f70d871 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,8 +16,8 @@ # Repository: github.com/umpire274/librius-dev-scripts.git # Access: private (owner/collaborators only) # ------------------------------------------------ -[submodule "tools_private"] - path = tools_private +[submodule "dev_tools"] + path = dev_tools url = git@github.com:umpire274/rust_dev_scripts.git # ================================================================ diff --git a/tools_private b/dev_tools similarity index 100% rename from tools_private rename to dev_tools From 064b6ec403091c6bfb878f7e4ab579bef2b640b4 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:39:43 +0200 Subject: [PATCH 03/12] refactor(utils): split monolithic mod.rs into focused submodules Extract verbose, print, log and import_helpers into dedicated files. Update mod.rs to declare all submodules and re-export symbols explicitly (no more blanket glob re-exports inside utils). - utils/verbose.rs: VERBOSE static, set_verbose(), is_verbose() - utils/print.rs: icons module, print_ok/err/warn/info() - utils/log.rs: now_str(), write_log() - utils/import_helpers.rs: open_import_file(), handle_import_result() - utils/mod.rs: aggregator with explicit pub use only All callers unchanged; public API surface identical. --- src/utils/import_helpers.rs | 47 +++++++++++ src/utils/log.rs | 59 +++++++++++++ src/utils/mod.rs | 161 +++++------------------------------- src/utils/print.rs | 45 ++++++++++ src/utils/verbose.rs | 20 +++++ 5 files changed, 192 insertions(+), 140 deletions(-) create mode 100644 src/utils/import_helpers.rs create mode 100644 src/utils/log.rs create mode 100644 src/utils/print.rs create mode 100644 src/utils/verbose.rs diff --git a/src/utils/import_helpers.rs b/src/utils/import_helpers.rs new file mode 100644 index 0000000..00455fa --- /dev/null +++ b/src/utils/import_helpers.rs @@ -0,0 +1,47 @@ +// ===================================================== +// Librius - utils/import_helpers.rs +// ----------------------------------------------------- +// Helper per la gestione dei file e dei risultati +// delle operazioni di import nel database. +// ===================================================== + +use crate::i18n::tr_with; +use crate::utils::print::print_err; +use rusqlite::Result as SqlResult; +use std::fs::File; +use std::io; + +/// Opens a file for import operations and prints a localized error message on failure. +pub fn open_import_file(file: &str) -> Result { + let file_display = file.to_string(); + + File::open(file).inspect_err(|e| { + print_err(&tr_with( + "import.error.open_failed", + &[("file", &file_display), ("error", &e.to_string())], + )); + }) +} + +/// Handles the result of a database insert operation for book import. +/// Increments counters and prints localized error messages if necessary. +pub fn handle_import_result( + result: &SqlResult, + imported: &mut u32, + failed: &mut u32, + title: &str, +) { + match result { + Ok(_) => { + *imported += 1; + } + Err(e) => { + *failed += 1; + print_err(&tr_with( + "import.error.insert_failed", + &[("title", title), ("error", &e.to_string())], + )); + } + } +} + diff --git a/src/utils/log.rs b/src/utils/log.rs new file mode 100644 index 0000000..64049f7 --- /dev/null +++ b/src/utils/log.rs @@ -0,0 +1,59 @@ +// ===================================================== +// Librius - utils/log.rs +// ----------------------------------------------------- +// Funzioni per la scrittura del log sul database SQLite +// e per la formattazione della data/ora corrente. +// ===================================================== + +use chrono::Local; +use rusqlite::{Connection, Result}; + +/// Returns the current local date-time in full ISO 8601 format. +/// +/// Example output: +/// ```text +/// 2025-10-13T21:32:07+02:00 +/// ``` +pub fn now_str() -> String { + Local::now().format("%+").to_string() +} + +/// Writes an entry into the 'log' table. +/// If the table does not exist, it will be created automatically. +/// +/// # Arguments +/// * `conn` - Active SQLite connection +/// * `operation` - Type of action (e.g., "PATCH_001", "ADD_BOOK") +/// * `target` - Logical target (e.g., "DB", "CONFIG", "BOOKS") +/// * `message` - Description of the operation +/// +/// # Example +/// ``` +/// use librius::utils::write_log; +/// use rusqlite::Connection; +/// // create an in-memory connection for the example +/// let conn = Connection::open_in_memory().unwrap(); +/// write_log(&conn, "ADD_BOOK", "BOOKS", "Inserted 'Dune'").unwrap(); +/// ``` +pub fn write_log(conn: &Connection, operation: &str, target: &str, message: &str) -> Result<()> { + // Ensure log table exists + conn.execute( + "CREATE TABLE IF NOT EXISTS log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + operation TEXT NOT NULL, + target TEXT DEFAULT '', + message TEXT NOT NULL + );", + [], + )?; + + let now = now_str(); + conn.execute( + "INSERT INTO log (date, operation, target, message) VALUES (?1, ?2, ?3, ?4)", + (&now, &operation, &target, &message), + )?; + + Ok(()) +} + diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2a2550f..8ceeb53 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,154 +1,35 @@ // ===================================================== -// Librius - Utilities module (directory layout) +// Librius - utils/mod.rs // ----------------------------------------------------- -// Contiene funzioni di supporto generali e costanti -// grafiche per output CLI. +// Modulo aggregatore: dichiara i sottomoduli e +// ri-esporta i simboli pubblici in modo esplicito. // ===================================================== + pub mod fields; +pub mod import_helpers; pub mod isbn; pub mod lang; +pub mod log; +pub mod print; pub mod table; +pub mod verbose; -pub use lang::lang_code_to_name; -pub use table::{build_table, build_vertical_table}; - -use crate::i18n::tr_with; -use chrono::Local; -use colored::*; -use rusqlite::Result as SqlResult; -use rusqlite::{Connection, Result}; -use std::fs::File; -use std::io; -use std::sync::OnceLock; - -static VERBOSE: OnceLock = OnceLock::new(); - -/// Enables verbose (debug) mode. -pub fn set_verbose(enabled: bool) { - let _ = VERBOSE.set(enabled); -} - -/// Returns true if verbose mode is active. -pub fn is_verbose() -> bool { - *VERBOSE.get().unwrap_or(&false) -} - -/// Returns the current local date-time in full ISO 8601 format. -/// -/// Example output: -/// ```text -/// 2025-10-13T21:32:07+02:00 -/// ``` -pub fn now_str() -> String { - Local::now().format("%+").to_string() -} +// --- re-export espliciti --- -/// Writes an entry into the 'log' table. -/// If the table does not exist, it will be created automatically. -/// -/// # Arguments -/// * `conn` - Active SQLite connection -/// * `operation` - Type of action (e.g., "PATCH_001", "ADD_BOOK") -/// * `target` - Logical target (e.g., "DB", "CONFIG", "BOOKS") -/// * `message` - Description of the operation -/// -/// # Example -/// ``` -/// use librius::utils::write_log; -/// use rusqlite::Connection; -/// // create an in-memory connection for the example -/// let conn = Connection::open_in_memory().unwrap(); -/// write_log(&conn, "ADD_BOOK", "BOOKS", "Inserted 'Dune'").unwrap(); -/// ``` -pub fn write_log(conn: &Connection, operation: &str, target: &str, message: &str) -> Result<()> { - // Ensure log table exists - conn.execute( - "CREATE TABLE IF NOT EXISTS log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, - operation TEXT NOT NULL, - target TEXT DEFAULT '', - message TEXT NOT NULL - );", - [], - )?; +// verbose +pub use verbose::{is_verbose, set_verbose}; - let now = now_str(); - conn.execute( - "INSERT INTO log (date, operation, target, message) VALUES (?1, ?2, ?3, ?4)", - (&now, &operation, &target, &message), - )?; +// print +pub use print::{icons, print_err, print_info, print_ok, print_warn}; - Ok(()) -} +// log +pub use log::{now_str, write_log}; -/// Modulo con le icone standard utilizzate nell'applicazione. -/// -/// Le emoji sono seguite da uno spazio per evitare problemi -/// di spaziatura nei terminali (⚠️, βœ…, ❌, πŸ“˜, ecc.). -pub mod icons { - pub const OK: &str = "βœ… "; - pub const ERR: &str = "❌ "; - pub const WARN: &str = "⚠️ "; - pub const INFO: &str = "πŸ“˜ "; -} +// import helpers +pub use import_helpers::{handle_import_result, open_import_file}; -/// Stampa un messaggio formattato come "OK" -pub fn print_ok(msg: &str, verbose: bool) { - if !verbose { - return; - } - println!("{}{}", icons::OK, msg.green().bold()); -} - -/// Stampa un messaggio di errore formattato -pub fn print_err(msg: &str) { - eprintln!("{}{}", icons::ERR, msg.red().bold()); -} - -/// Stampa un messaggio di avviso formattato -pub fn print_warn(msg: &str) { - println!("{}{}", icons::WARN, msg.yellow().bold()); -} - -/// Stampa un messaggio informativo -pub fn print_info(msg: &str, verbose: bool) { - if !verbose { - return; - } - println!("{}{}", icons::INFO, msg.blue().bold()); -} - -/// Opens a file for import operations and prints a localized error message on failure. -pub fn open_import_file(file: &str) -> Result { - let file_display = file.to_string(); - - File::open(file).inspect_err(|e| { - print_err(&tr_with( - "import.error.open_failed", - &[("file", &file_display), ("error", &e.to_string())], - )); - }) -} +// lang +pub use lang::lang_code_to_name; -/// Handles the result of a database insert operation for book import. -/// Increments counters and prints localized error messages if necessary. -pub fn handle_import_result( - result: &SqlResult, - imported: &mut u32, - failed: &mut u32, - title: &str, -) { - match result { - Ok(_) => { - *imported += 1; - } - Err(e) => { - *failed += 1; - print_err(&tr_with( - "import.error.insert_failed", - &[("title", title), ("error", &e.to_string())], - )); - } - } -} +// table +pub use table::{build_table, build_vertical_table}; diff --git a/src/utils/print.rs b/src/utils/print.rs new file mode 100644 index 0000000..3f09c6c --- /dev/null +++ b/src/utils/print.rs @@ -0,0 +1,45 @@ +// ===================================================== +// Librius - utils/print.rs +// ----------------------------------------------------- +// Icone standard e funzioni di output formattato CLI. +// ===================================================== + +use colored::*; + +/// Modulo con le icone standard utilizzate nell'applicazione. +/// +/// Le emoji sono seguite da uno spazio per evitare problemi +/// di spaziatura nei terminali (⚠️, βœ…, ❌, πŸ“˜, ecc.). +pub mod icons { + pub const OK: &str = "βœ… "; + pub const ERR: &str = "❌ "; + pub const WARN: &str = "⚠️ "; + pub const INFO: &str = "πŸ“˜ "; +} + +/// Stampa un messaggio formattato come "OK" (solo in modalitΓ  verbose) +pub fn print_ok(msg: &str, verbose: bool) { + if !verbose { + return; + } + println!("{}{}", icons::OK, msg.green().bold()); +} + +/// Stampa un messaggio di errore formattato +pub fn print_err(msg: &str) { + eprintln!("{}{}", icons::ERR, msg.red().bold()); +} + +/// Stampa un messaggio di avviso formattato +pub fn print_warn(msg: &str) { + println!("{}{}", icons::WARN, msg.yellow().bold()); +} + +/// Stampa un messaggio informativo (solo in modalitΓ  verbose) +pub fn print_info(msg: &str, verbose: bool) { + if !verbose { + return; + } + println!("{}{}", icons::INFO, msg.blue().bold()); +} + diff --git a/src/utils/verbose.rs b/src/utils/verbose.rs new file mode 100644 index 0000000..6e43255 --- /dev/null +++ b/src/utils/verbose.rs @@ -0,0 +1,20 @@ +// ===================================================== +// Librius - utils/verbose.rs +// ----------------------------------------------------- +// Gestione della modalitΓ  verbose (debug) globale. +// ===================================================== + +use std::sync::OnceLock; + +static VERBOSE: OnceLock = OnceLock::new(); + +/// Enables verbose (debug) mode. +pub fn set_verbose(enabled: bool) { + let _ = VERBOSE.set(enabled); +} + +/// Returns true if verbose mode is active. +pub fn is_verbose() -> bool { + *VERBOSE.get().unwrap_or(&false) +} + From 9408139c83253da83da3163ce165d98c33057f0f Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:43:05 +0200 Subject: [PATCH 04/12] refactor(cli): move EDITABLE_FIELDS to cli/fields.rs, remove dead Commands enum - Create cli/fields.rs: EDITABLE_FIELDS is a CLI concern, not a generic utility - Update cli/mod.rs: declare fields submodule, remove dead Commands enum (only 3 of 8 commands were represented, never used by dispatch) - Update cli/args.rs, commands/edit_book.rs: import from crate::cli::fields - Refactor commands/config.rs: replace Commands enum parameter with explicit bool args (init, print, edit, editor) - cleaner API, no enum dependency - Update cli/dispatch.rs: call handle_config with direct bool params - Remove utils/fields.rs (deleted) --- src/cli/args.rs | 2 +- src/cli/dispatch.rs | 10 +- src/{utils => cli}/fields.rs | 12 +++ src/cli/mod.rs | 17 +-- src/commands/config.rs | 196 +++++++++++++++++------------------ src/commands/edit_book.rs | 2 +- src/utils/mod.rs | 1 - 7 files changed, 114 insertions(+), 126 deletions(-) rename src/{utils => cli}/fields.rs (54%) diff --git a/src/cli/args.rs b/src/cli/args.rs index da02091..545bb89 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -1,4 +1,4 @@ -use crate::fields::EDITABLE_FIELDS; +use crate::cli::fields::EDITABLE_FIELDS; use crate::i18n::{tr, tr_s}; use clap::{Arg, ArgAction, Command}; diff --git a/src/cli/dispatch.rs b/src/cli/dispatch.rs index e1eb625..ee99048 100644 --- a/src/cli/dispatch.rs +++ b/src/cli/dispatch.rs @@ -1,4 +1,4 @@ -use crate::cli::{Commands, build_cli}; +use crate::cli::build_cli; use crate::i18n::tr; use crate::utils::print_err; use crate::{AppConfig, handle_config, handle_edit_book, handle_list, handle_search, tr_with}; @@ -31,13 +31,7 @@ pub fn run_cli( let edit = sub_m.get_flag("edit"); let editor = sub_m.get_one::("editor").cloned(); - let cmd = Commands::Config { - init, - print, - edit, - editor, - }; - Ok(handle_config(&cmd)?) + Ok(handle_config(init, print, edit, editor)?) } else if let Some(("db", sub_m)) = matches.subcommand() { let init = sub_m.get_flag("init"); let reset = sub_m.get_flag("reset"); diff --git a/src/utils/fields.rs b/src/cli/fields.rs similarity index 54% rename from src/utils/fields.rs rename to src/cli/fields.rs index 80954ad..1015c91 100644 --- a/src/utils/fields.rs +++ b/src/cli/fields.rs @@ -1,3 +1,14 @@ +// ===================================================== +// Librius - cli/fields.rs +// ----------------------------------------------------- +// Definizione dei campi editabili di un libro. +// Usata da args.rs (costruzione CLI) e da edit_book.rs +// (logica di comando). È una preoccupazione CLI, non +// una utility generica. +// ===================================================== + +/// Lista dei campi editabili di un libro. +/// Ogni elemento Γ¨ una tupla (nome_campo, chiave_i18n, shortcut). pub const EDITABLE_FIELDS: &[(&str, &str, char)] = &[ ("title", "help.edit.book.title", 't'), ("author", "help.edit.book.author", 'a'), @@ -12,3 +23,4 @@ pub const EDITABLE_FIELDS: &[(&str, &str, char)] = &[ ("row", "help.edit.book.row", 'w'), ("position", "help.edit.book.position", 'o'), ]; + diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f213237..7518202 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,25 +1,12 @@ pub mod args; pub mod dispatch; +pub mod fields; pub use args::build_cli; pub use dispatch::run_cli; - -use clap::Subcommand; +pub use fields::EDITABLE_FIELDS; /// Parsing CLI pub fn parse_cli() -> clap::ArgMatches { build_cli().get_matches() } - -/// Enum di compatibilitΓ  con i moduli dei comandi -#[derive(Subcommand)] -pub enum Commands { - List, - Config { - init: bool, - print: bool, - edit: bool, - editor: Option, - }, - Help, -} diff --git a/src/commands/config.rs b/src/commands/config.rs index fc61f12..618ee1e 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,4 +1,3 @@ -use crate::cli::Commands; use crate::config; use crate::i18n::{tr, tr_with}; use crate::utils::{print_err, print_info, print_ok, print_warn}; @@ -6,121 +5,118 @@ use std::fs; use std::path::Path; use std::process::Command; -pub fn handle_config(cmd: &Commands) -> rusqlite::Result<()> { +pub fn handle_config( + init: bool, + print: bool, + edit: bool, + editor: Option, +) -> rusqlite::Result<()> { let config_path = config::config_file_path(); - if let Commands::Config { - print, - init, - edit, - editor, - } = cmd - { - // --print - if *print { - if config_path.exists() { - match fs::read_to_string(&config_path) { - Ok(contents) => { - println!(); - print_info(&tr("config.schema.list"), true); - println!("\n{}", contents); - } - Err(e) => { - print_err(&tr_with("config.open.failed", &[("error", &e.to_string())])) - } + // --print + if print { + if config_path.exists() { + match fs::read_to_string(&config_path) { + Ok(contents) => { + println!(); + print_info(&tr("config.schema.list"), true); + println!("\n{}", contents); } - } else { - print_warn(&tr("config.file.not_found")); - } - } - - // --init - if *init { - if config_path.exists() { - print_warn(&tr("config.file.exists")); - } else { - if let Err(e) = config::load_or_init() { - print_err(&tr_with( - "config.file.init_failed", - &[("error", &e.to_string())], - )); + Err(e) => { + print_err(&tr_with("config.open.failed", &[("error", &e.to_string())])) } - print_ok( - &tr_with( - "config.file.created", - &[("path", &config_path.display().to_string())], - ), - true, - ); } + } else { + print_warn(&tr("config.file.not_found")); } + } - // --edit - if *edit { - println!(); - if !config_path.exists() - && let Err(e) = config::load_or_init() - { + // --init + if init { + if config_path.exists() { + print_warn(&tr("config.file.exists")); + } else { + if let Err(e) = config::load_or_init() { print_err(&tr_with( - "config.file.create_failed", + "config.file.init_failed", &[("error", &e.to_string())], )); } + print_ok( + &tr_with( + "config.file.created", + &[("path", &config_path.display().to_string())], + ), + true, + ); + } + } - // User-requested editor (if provided) - let requested_editor = editor.clone(); - - let default_editor = std::env::var("EDITOR") - .or_else(|_| std::env::var("VISUAL")) - .unwrap_or_else(|_| { - if cfg!(target_os = "windows") { - "notepad".to_string() - } else { - "nano".to_string() - } - }); + // --edit + if edit { + println!(); + if !config_path.exists() + && let Err(e) = config::load_or_init() + { + print_err(&tr_with( + "config.file.create_failed", + &[("error", &e.to_string())], + )); + } - // Use the requested editor if available, otherwise fall back - let editor_to_use = requested_editor.unwrap_or_else(|| default_editor.clone()); - let editor_path = Path::new(&editor_to_use); + // User-requested editor (if provided) + let requested_editor = editor.clone(); - let status = Command::new(editor_path).arg(&config_path).status(); - match status { - Ok(s) if s.success() => { - print_ok( - &tr_with( - "config.file.edited_with", - &[("editor", &editor_path.display().to_string())], - ), - true, - ); + let default_editor = std::env::var("EDITOR") + .or_else(|_| std::env::var("VISUAL")) + .unwrap_or_else(|_| { + if cfg!(target_os = "windows") { + "notepad".to_string() + } else { + "nano".to_string() } - Ok(_) | Err(_) => { - print_err(&tr_with( - "config.file.editor_not_available", - &[ - ("editor", &editor_path.display().to_string()), - ("defaultEditor", &default_editor), - ], - )); - // Retry with the default editor - let fallback_status = Command::new(&default_editor).arg(&config_path).status(); - match fallback_status { - Ok(s) if s.success() => { - print_ok( - &tr_with( - "config.file.edited_fallback", - &[("editor", &default_editor)], - ), - true, - ); - } - Ok(_) | Err(_) => { - print_err(&tr_with( - "config.file.edit_failed_fallback", + }); + + // Use the requested editor if available, otherwise fall back + let editor_to_use = requested_editor.unwrap_or_else(|| default_editor.clone()); + let editor_path = Path::new(&editor_to_use); + + let status = Command::new(editor_path).arg(&config_path).status(); + match status { + Ok(s) if s.success() => { + print_ok( + &tr_with( + "config.file.edited_with", + &[("editor", &editor_path.display().to_string())], + ), + true, + ); + } + Ok(_) | Err(_) => { + print_err(&tr_with( + "config.file.editor_not_available", + &[ + ("editor", &editor_path.display().to_string()), + ("defaultEditor", &default_editor), + ], + )); + // Retry with the default editor + let fallback_status = Command::new(&default_editor).arg(&config_path).status(); + match fallback_status { + Ok(s) if s.success() => { + print_ok( + &tr_with( + "config.file.edited_fallback", &[("editor", &default_editor)], - )); - } + ), + true, + ); + } + Ok(_) | Err(_) => { + print_err(&tr_with( + "config.file.edit_failed_fallback", + &[("editor", &default_editor)], + )); } } } diff --git a/src/commands/edit_book.rs b/src/commands/edit_book.rs index 1170dc1..b77694f 100644 --- a/src/commands/edit_book.rs +++ b/src/commands/edit_book.rs @@ -1,5 +1,5 @@ use crate::db::books::{get_book_fields, update_book_by_id, update_book_by_isbn}; -use crate::fields::EDITABLE_FIELDS; +use crate::cli::fields::EDITABLE_FIELDS; use crate::{lang_code_to_name, print_err, print_info, print_ok, print_warn, tr, tr_with}; use rusqlite::Connection; use std::collections::HashMap; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8ceeb53..35f4689 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,7 +5,6 @@ // ri-esporta i simboli pubblici in modo esplicito. // ===================================================== -pub mod fields; pub mod import_helpers; pub mod isbn; pub mod lang; From 1ddc61ac24a2ebebfbfd3add9fcd7bc468a340e4 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:45:46 +0200 Subject: [PATCH 05/12] refactor(models): separate data model from presentation layer - models/book.rs: pure data model only (Book struct + from_row + Serde) Remove tabled/i18n dependencies - models/display.rs: new file with BookFull, BookShort and their Tabled implementations (presentation concern, depends on i18n) - models/mod.rs: declare display submodule, re-export Book/BookFull/BookShort - commands/list.rs: update imports to explicit paths (models::book::Book, models::display::{BookFull, BookShort}) --- src/commands/list.rs | 3 +- src/models/book.rs | 85 +++++-------------------------------------- src/models/display.rs | 81 +++++++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 4 +- 4 files changed, 95 insertions(+), 78 deletions(-) create mode 100644 src/models/display.rs diff --git a/src/commands/list.rs b/src/commands/list.rs index 438e880..bb9856c 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,4 +1,5 @@ -use crate::book::{Book, BookFull, BookShort}; +use crate::models::book::Book; +use crate::models::display::{BookFull, BookShort}; use crate::i18n::tr; use crate::isbn::normalize_isbn; use crate::utils::{build_table, build_vertical_table, print_err}; diff --git a/src/models/book.rs b/src/models/book.rs index dec2725..a0da5ee 100644 --- a/src/models/book.rs +++ b/src/models/book.rs @@ -1,8 +1,14 @@ -use crate::i18n::tr; +// ===================================================== +// Librius - models/book.rs +// ----------------------------------------------------- +// Modello dati puro per un libro. +// Nessuna dipendenza da i18n o tabled: solo dati, +// serializzazione e costruzione da riga SQLite. +// ===================================================== + use chrono::{DateTime, Utc}; +use rusqlite::Row; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use tabled::Tabled; #[derive(Debug, Serialize, Deserialize)] pub struct Book { @@ -23,79 +29,6 @@ pub struct Book { pub added_at: Option>, } -pub struct BookFull<'a>(pub &'a Book); -pub struct BookShort<'a>(pub &'a Book); - -impl<'a> Tabled for BookFull<'a> { - const LENGTH: usize = 10; - - fn fields(&self) -> Vec> { - let b = self.0; - /*let added_date = b - .added_at - .as_ref() - .map(|d| d.format("%Y-%m-%d").to_string()) - .unwrap_or_else(|| "-".into());*/ - - vec![ - Cow::from(b.id.map(|v| v.to_string()).unwrap_or_default()), - Cow::from(&b.title), - Cow::from(&b.author), - Cow::from(&b.editor), - Cow::from(b.year.to_string()), - Cow::from(b.isbn.to_string()), - Cow::from(b.language.as_deref().unwrap_or("-")), - Cow::from(b.room.as_deref().unwrap_or("-")), - Cow::from(b.shelf.as_deref().unwrap_or("-")), - Cow::from(b.position.as_deref().unwrap_or("-")), - ] - } - - fn headers() -> Vec> { - vec![ - Cow::from(tr("list.header.id")), - Cow::from(tr("list.header.title")), - Cow::from(tr("list.header.author")), - Cow::from(tr("list.header.editor")), - Cow::from(tr("list.header.year")), - Cow::from(tr("list.header.ISBN")), - Cow::from(tr("list.header.language")), - Cow::from(tr("list.header.room")), - Cow::from(tr("list.header.shelf")), - Cow::from(tr("list.header.position")), - ] - } -} - -impl<'a> Tabled for BookShort<'a> { - const LENGTH: usize = 6; - - fn fields(&self) -> Vec> { - let b = self.0; - vec![ - Cow::from(b.id.map(|v| v.to_string()).unwrap_or_default()), - Cow::from(&b.title), - Cow::from(&b.author), - Cow::from(&b.editor), - Cow::from(b.year.to_string()), - Cow::from(b.isbn.to_string()), - ] - } - - fn headers() -> Vec> { - vec![ - Cow::from(tr("list.header.id")), - Cow::from(tr("list.header.title")), - Cow::from(tr("list.header.author")), - Cow::from(tr("list.header.editor")), - Cow::from(tr("list.header.year")), - Cow::from(tr("list.header.ISBN")), - ] - } -} - -use rusqlite::Row; - impl Book { pub fn from_row(row: &Row) -> rusqlite::Result { Ok(Self { diff --git a/src/models/display.rs b/src/models/display.rs new file mode 100644 index 0000000..fda81a3 --- /dev/null +++ b/src/models/display.rs @@ -0,0 +1,81 @@ +// ===================================================== +// Librius - models/display.rs +// ----------------------------------------------------- +// Wrapper di presentazione per il tipo Book. +// Separa la logica di visualizzazione (Tabled + i18n) +// dal modello dati puro definito in book.rs. +// ===================================================== + +use crate::i18n::tr; +use crate::models::book::Book; +use std::borrow::Cow; +use tabled::Tabled; + +/// Vista completa del libro (10 colonne) per la tabella `list`. +pub struct BookFull<'a>(pub &'a Book); + +/// Vista ridotta del libro (6 colonne) per `list --short`. +pub struct BookShort<'a>(pub &'a Book); + +impl<'a> Tabled for BookFull<'a> { + const LENGTH: usize = 10; + + fn fields(&self) -> Vec> { + let b = self.0; + vec![ + Cow::from(b.id.map(|v| v.to_string()).unwrap_or_default()), + Cow::from(&b.title), + Cow::from(&b.author), + Cow::from(&b.editor), + Cow::from(b.year.to_string()), + Cow::from(b.isbn.to_string()), + Cow::from(b.language.as_deref().unwrap_or("-")), + Cow::from(b.room.as_deref().unwrap_or("-")), + Cow::from(b.shelf.as_deref().unwrap_or("-")), + Cow::from(b.position.as_deref().unwrap_or("-")), + ] + } + + fn headers() -> Vec> { + vec![ + Cow::from(tr("list.header.id")), + Cow::from(tr("list.header.title")), + Cow::from(tr("list.header.author")), + Cow::from(tr("list.header.editor")), + Cow::from(tr("list.header.year")), + Cow::from(tr("list.header.ISBN")), + Cow::from(tr("list.header.language")), + Cow::from(tr("list.header.room")), + Cow::from(tr("list.header.shelf")), + Cow::from(tr("list.header.position")), + ] + } +} + +impl<'a> Tabled for BookShort<'a> { + const LENGTH: usize = 6; + + fn fields(&self) -> Vec> { + let b = self.0; + vec![ + Cow::from(b.id.map(|v| v.to_string()).unwrap_or_default()), + Cow::from(&b.title), + Cow::from(&b.author), + Cow::from(&b.editor), + Cow::from(b.year.to_string()), + Cow::from(b.isbn.to_string()), + ] + } + + fn headers() -> Vec> { + vec![ + Cow::from(tr("list.header.id")), + Cow::from(tr("list.header.title")), + Cow::from(tr("list.header.author")), + Cow::from(tr("list.header.editor")), + Cow::from(tr("list.header.year")), + Cow::from(tr("list.header.ISBN")), + ] + } +} + diff --git a/src/models/mod.rs b/src/models/mod.rs index 3df16c5..85fdcd9 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ pub mod book; +pub mod display; -pub use book::{Book, BookFull, BookShort}; +pub use book::Book; +pub use display::{BookFull, BookShort}; From c191f6cdde245966aa608060e832bf67c084c317 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:48:25 +0200 Subject: [PATCH 06/12] =?UTF-8?q?refactor(db):=20rename=20modules=20and=20?= =?UTF-8?q?absorb=20search=20into=20books=20-=20load=5Fdb.rs=20=20?= =?UTF-8?q?=E2=86=92=20connection.rs=20=20(clearer=20name=20for=20DB=20con?= =?UTF-8?q?nection=20management)=20-=20migrate=5Fdb.rs=20=E2=86=92=20migra?= =?UTF-8?q?tions.rs=20(clearer=20name=20for=20migration=20logic)=20-=20sea?= =?UTF-8?q?rch.rs=20absorbed=20into=20books.rs:=20search=5Fbooks()=20belon?= =?UTF-8?q?gs=20with=20book=20operations=20-=20db/mod.rs:=20update=20pub?= =?UTF-8?q?=20mod/use=20to=20new=20names,=20expose=20search=5Fbooks=20from?= =?UTF-8?q?=20books=20-=20main.rs:=20update=20direct=20reference=20db::mig?= =?UTF-8?q?rate=5Fdb=20=E2=86=92=20db::migrations=20-=20Old=20files=20remo?= =?UTF-8?q?ved:=20load=5Fdb.rs,=20migrate=5Fdb.rs,=20search.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/db/books.rs | 28 +++++++++++++++++++++++++ src/db/{load_db.rs => connection.rs} | 15 ++++++------- src/db/{migrate_db.rs => migrations.rs} | 0 src/db/mod.rs | 21 +++++++------------ src/db/search.rs | 28 ------------------------- src/main.rs | 2 +- 6 files changed, 44 insertions(+), 50 deletions(-) rename src/db/{load_db.rs => connection.rs} (93%) rename src/db/{migrate_db.rs => migrations.rs} (100%) delete mode 100644 src/db/search.rs diff --git a/src/db/books.rs b/src/db/books.rs index 19476b7..ab14236 100644 --- a/src/db/books.rs +++ b/src/db/books.rs @@ -1,3 +1,4 @@ +use crate::models::Book; use rusqlite::types::ValueRef; use rusqlite::{Connection, Result, params, params_from_iter}; use std::collections::HashMap; @@ -92,3 +93,30 @@ pub fn get_book_fields( Ok(old_values) } + +/// Search books by a full-text query across title, author, editor, genre and language. +pub fn search_books(conn: &Connection, query: &str) -> Result> { + let like = format!("%{}%", query); + + let mut stmt = conn.prepare( + r#" + SELECT id, title, author, editor, year, isbn, language, pages, + genre, summary, room, shelf, row, position, added_at + FROM books + WHERE title LIKE ?1 + OR author LIKE ?1 + OR editor LIKE ?1 + OR genre LIKE ?1 + OR language LIKE ?1 + ORDER BY title COLLATE NOCASE ASC; + "#, + )?; + + let rows = stmt.query_map([like], Book::from_row)?; + + let mut out = Vec::new(); + for r in rows { + out.push(r?); + } + Ok(out) +} diff --git a/src/db/load_db.rs b/src/db/connection.rs similarity index 93% rename from src/db/load_db.rs rename to src/db/connection.rs index 3ac9fb8..025944b 100644 --- a/src/db/load_db.rs +++ b/src/db/connection.rs @@ -1,5 +1,5 @@ use crate::config::AppConfig; -use crate::db::migrate_db; +use crate::db::migrations; use crate::i18n::{tr, tr_with}; use crate::utils::{is_verbose, print_err, print_info, print_ok, write_log}; use rusqlite::{Connection, Result}; @@ -11,12 +11,12 @@ use std::path::PathBuf; /// Restituisce il percorso completo del file di database. /// /// La logica Γ¨: -/// - Se esiste `LIBRIUS_DB_PATH` nelle variabili d’ambiente β†’ usa quello. -/// - Altrimenti crea (se necessario) la directory predefinita dell’app: +/// - Se esiste `LIBRIUS_DB_PATH` nelle variabili d'ambiente β†’ usa quello. +/// - Altrimenti crea (se necessario) la directory predefinita dell'app: /// - Linux/macOS: `~/.local/share/librius/librius.db` /// - Windows: `%APPDATA%\\Librius\\librius.db` pub fn get_db_path() -> PathBuf { - // 1️⃣ Se definita, rispetta la variabile d’ambiente + // 1️⃣ Se definita, rispetta la variabile d'ambiente if let Ok(custom) = std::env::var("LIBRIUS_DB_PATH") { return PathBuf::from(custom); } @@ -100,18 +100,18 @@ pub fn start_db(config: &AppConfig) -> Result { } // 6️⃣ Esegui eventuali migrazioni - match migrate_db::run_migrations(&conn) { + match migrations::run_migrations(&conn) { Err(e) => { print_err(&tr_with("db.migrate.failed", &[("error", &e.to_string())])); let _ = write_log(&conn, "DB_MIGRATION_FAIL", "DB", &e.to_string()); } Ok(result) => match result { - migrate_db::MigrationResult::Applied(patches) => { + migrations::MigrationResult::Applied(patches) => { print_ok(&tr("db.migrate.applied"), is_verbose()); let msg = &tr_with("log.db.patch_applied", &[("patches", &patches.join(", "))]); let _ = write_log(&conn, "DB_MIGRATION_OK", "DB", msg); } - migrate_db::MigrationResult::None => { + migrations::MigrationResult::None => { print_ok(&tr("db.schema.already_update"), is_verbose()); } }, @@ -149,3 +149,4 @@ pub fn ensure_schema(conn: &Connection) -> Result<()> { )?; Ok(()) } + diff --git a/src/db/migrate_db.rs b/src/db/migrations.rs similarity index 100% rename from src/db/migrate_db.rs rename to src/db/migrations.rs diff --git a/src/db/mod.rs b/src/db/mod.rs index 42d0df2..b2230d0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,19 +1,12 @@ //! Database initialization utilities for Librius. //! -//! This module provides a small helper to initialize (or open) the SQLite -//! database used by the application. The `init_db` function ensures the -//! required `books` table exists and returns an active `rusqlite::Connection`. -//! -//! The schema is intentionally simple and stores basic metadata for each -//! models (title, author, year, isbn and a timestamp when the record was -//! added). +//! This module provides helpers to initialize (or open) the SQLite database, +//! run schema migrations and perform book-related queries. pub mod books; -pub mod load_db; -pub mod migrate_db; -pub mod search; +pub mod connection; +pub mod migrations; -pub use books::{get_book_fields, update_book_by_id, update_book_by_isbn}; -pub use load_db::{ensure_schema, get_db_path, init_db, start_db}; -pub use migrate_db::{MigrationResult, run_migrations}; -pub use search::search_books; +pub use books::{get_book_fields, search_books, update_book_by_id, update_book_by_isbn}; +pub use connection::{ensure_schema, get_db_path, init_db, start_db}; +pub use migrations::{MigrationResult, run_migrations}; diff --git a/src/db/search.rs b/src/db/search.rs deleted file mode 100644 index f6d790b..0000000 --- a/src/db/search.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::models::Book; -use rusqlite::{Connection, Result}; - -pub fn search_books(conn: &Connection, query: &str) -> Result> { - let like = format!("%{}%", query); - - let mut stmt = conn.prepare( - r#" - SELECT id, title, author, editor, year, isbn, language, pages, - genre, summary, room, shelf, row, position, added_at - FROM books - WHERE title LIKE ?1 - OR author LIKE ?1 - OR editor LIKE ?1 - OR genre LIKE ?1 - OR language LIKE ?1 - ORDER BY title COLLATE NOCASE ASC; - "#, - )?; - - let rows = stmt.query_map([like], Book::from_row)?; - - let mut out = Vec::new(); - for r in rows { - out.push(r?); - } - Ok(out) -} diff --git a/src/main.rs b/src/main.rs index f86c1a4..f71eccb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,7 +64,7 @@ fn main() { // ------------------------------------------------------------ // 5️⃣ Esegue migrazioni DB e config // ------------------------------------------------------------ - if let Err(e) = db::migrate_db::run_migrations(&conn) { + if let Err(e) = db::migrations::run_migrations(&conn) { print_err(&tr_with("db.migrate.failed", &[("error", &e.to_string())])); } else { print_ok(&tr("db.schema.verified"), is_verbose()); From da380098715f8940678486a452d855cb774c6bb7 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:50:59 +0200 Subject: [PATCH 07/12] refactor(commands): remove unused wrapper, deduplicate schema and row mapping - Remove commands/add.rs: 18-line wrapper that just called handle_add_book(). dispatch.rs already invokes handle_add_book() directly (dead code) - commands/mod.rs: drop pub mod/use for removed add module - commands/list.rs: simplify row_to_book() to delegate to Book::from_row() and only apply ISBN hyphen formatting on top; remove unused chrono imports - commands/db.rs: replace inline create_schema() duplicate (tripled) with a call to db::connection::ensure_schema() - single source of truth for schema --- src/commands/add.rs | 17 --------------- src/commands/db.rs | 44 ++------------------------------------ src/commands/list.rs | 51 +++++++------------------------------------- src/commands/mod.rs | 2 -- 4 files changed, 10 insertions(+), 104 deletions(-) delete mode 100644 src/commands/add.rs diff --git a/src/commands/add.rs b/src/commands/add.rs deleted file mode 100644 index e698ff7..0000000 --- a/src/commands/add.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::commands::add_book::handle_add_book; -use crate::i18n::tr; -use clap::ArgMatches; -use rusqlite::Connection; - -pub fn handle_add( - conn: &Connection, - matches: &ArgMatches, -) -> Result<(), Box> { - if let Some(("book", sub_m)) = matches.subcommand() { - let isbn = sub_m.get_one::("isbn").unwrap(); - handle_add_book(conn, isbn)?; - } else { - println!("{}", tr("help.add.usage")); - } - Ok(()) -} diff --git a/src/commands/db.rs b/src/commands/db.rs index 84cd10b..8e5dac5 100644 --- a/src/commands/db.rs +++ b/src/commands/db.rs @@ -1,3 +1,4 @@ +use crate::db::connection::ensure_schema; use crate::{AppConfig, print_err, print_ok, print_warn, tr, tr_with}; use rusqlite::Connection; use std::error::Error; @@ -43,7 +44,7 @@ fn init_db(config: &AppConfig) -> Result<(), Box> { } let conn = Connection::open(path)?; - create_schema(&conn)?; + ensure_schema(&conn)?; print_ok( &tr_with("db_init_done", &[("path", &path.to_string_lossy())]), true, @@ -51,47 +52,6 @@ fn init_db(config: &AppConfig) -> Result<(), Box> { Ok(()) } -/// Crea le tabelle di base nel database appena inizializzato. -/// -/// Al momento include: -/// - books β†’ archivio principale dei libri -/// - log β†’ tabella di log operazioni -fn create_schema(conn: &Connection) -> Result<(), Box> { - // Tabella principale "books" - conn.execute_batch( - " - CREATE TABLE IF NOT EXISTS books ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - author TEXT NOT NULL, - editor TEXT NOT NULL, - year INTEGER NOT NULL, - isbn TEXT NOT NULL, - language TEXT, - pages INTEGER, - genre TEXT, - summary TEXT, - room TEXT, - shelf TEXT, - row TEXT, - position TEXT, - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, - operation TEXT NOT NULL, - target TEXT DEFAULT '', - message TEXT NOT NULL - ); - ", - )?; - - print_ok(&tr("db.schema.created"), true); - Ok(()) -} - fn copy_db(config: &AppConfig, dest: &str) -> Result<(), Box> { let src = Path::new(&config.database); if !src.exists() { diff --git a/src/commands/list.rs b/src/commands/list.rs index bb9856c..e5daedc 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -3,58 +3,23 @@ use crate::models::display::{BookFull, BookShort}; use crate::i18n::tr; use crate::isbn::normalize_isbn; use crate::utils::{build_table, build_vertical_table, print_err}; -use chrono::{DateTime, NaiveDateTime, Utc}; use rusqlite::types::ToSql; use rusqlite::{Connection, Row}; use std::error::Error; -fn parse_added_at(s: &str) -> Option> { - if let Ok(dt) = DateTime::parse_from_rfc3339(s) { - return Some(dt.with_timezone(&Utc)); - } - if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") { - return Some(DateTime::from_naive_utc_and_offset(naive, Utc)); - } - if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") { - return Some(DateTime::from_naive_utc_and_offset(naive, Utc)); - } - None -} - -// Helper to map a rusqlite::Row into a Book instance. +/// Maps a rusqlite::Row into a Book, applying ISBN hyphen formatting on top of +/// the base `Book::from_row()` constructor. fn row_to_book(row: &Row) -> rusqlite::Result { - let added_at_str: Option = row.get("added_at")?; - let parsed_added_at = added_at_str.as_deref().and_then(parse_added_at); - - // Recupera ISBN dal DB (senza trattini) - let isbn_plain: String = row.get("isbn")?; - - // Prova a formattarlo con trattini (se valido) - let isbn_formatted = match normalize_isbn(&isbn_plain, false) { + let mut book = Book::from_row(row)?; + // Format ISBN with hyphens for display; fall back to plain on error. + book.isbn = match normalize_isbn(&book.isbn, false) { Ok(formatted) => formatted, Err(e) => { print_err(&e.to_string()); - isbn_plain.clone() - } // fallback in caso di ISBN non valido + book.isbn + } }; - - Ok(Book { - id: row.get("id")?, - title: row.get("title")?, - author: row.get("author")?, - editor: row.get("editor")?, - year: row.get("year")?, - isbn: isbn_formatted, - language: row.get("language")?, - pages: row.get("pages")?, - genre: row.get("genre")?, - summary: row.get("summary")?, - room: row.get("room")?, - shelf: row.get("shelf")?, - row: row.get("row")?, - position: row.get("position")?, - added_at: parsed_added_at, - }) + Ok(book) } /// Handle the `list` subcommand. diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e5ca38e..963dac4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,7 +4,6 @@ //! `list`) so documentation generators can show the available commands and //! their handlers. -pub mod add; pub mod add_book; pub mod backup; pub mod config; @@ -16,7 +15,6 @@ pub mod import; pub mod list; pub mod search_book; -pub use add::handle_add; pub use add_book::handle_add_book; pub use backup::handle_backup; pub use config::handle_config; From 51e97d6c0b49f3c65042d693ba1bec7915dddadb Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:54:54 +0200 Subject: [PATCH 08/12] refactor(lib): replace glob re-exports with explicit public API; fix all internal imports - lib.rs: remove pub use commands/config/db/i18n/models/utils::* Replace with explicit re-exports (AppConfig, load_or_init, init_db, start_db, Book, load_language, tr, tr_s, tr_with) - clear public API - main.rs: remove duplicate run_migrations() call (already run in start_db) - All internal modules now use explicit crate paths instead of crate root shortcuts: * utils/isbn.rs: crate::tr_with -> crate::i18n::tr_with * utils/table.rs: crate::{print_warn,tr} -> crate::utils/i18n explicit paths * commands/add_book.rs: normalize_isbn, is_verbose, print_* via explicit paths * commands/del_book.rs: print_*, write_log, tr_with via explicit paths * commands/edit_book.rs: lang_code_to_name, print_*, tr/tr_with explicit * commands/import.rs: Book from models, print_err from utils * commands/search_book.rs: print_warn from utils * commands/list.rs: normalize_isbn from utils::isbn * commands/db.rs: AppConfig, tr/tr_with, print_* all explicit * cli/dispatch.rs: AppConfig, commands, i18n all explicit - tests/isbn_tests.rs: update import path to librius::utils::isbn - utils/isbn.rs doctest: update example path to utils::isbn::normalize_isbn --- src/cli/dispatch.rs | 5 +++-- src/commands/add_book.rs | 7 +++---- src/commands/db.rs | 4 +++- src/commands/del_book.rs | 3 ++- src/commands/edit_book.rs | 3 ++- src/commands/import.rs | 4 ++-- src/commands/list.rs | 2 +- src/commands/search_book.rs | 2 +- src/lib.rs | 27 +++++++++++++++------------ src/main.rs | 9 ++------- src/utils/isbn.rs | 4 ++-- src/utils/table.rs | 3 ++- tests/isbn_tests.rs | 2 +- 13 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/cli/dispatch.rs b/src/cli/dispatch.rs index ee99048..5156e38 100644 --- a/src/cli/dispatch.rs +++ b/src/cli/dispatch.rs @@ -1,7 +1,8 @@ use crate::cli::build_cli; -use crate::i18n::tr; +use crate::commands::{handle_config, handle_edit_book, handle_list, handle_search}; +use crate::config::AppConfig; +use crate::i18n::{tr, tr_with}; use crate::utils::print_err; -use crate::{AppConfig, handle_config, handle_edit_book, handle_list, handle_search, tr_with}; use rusqlite::Connection; /// Dispatch principale dei comandi diff --git a/src/commands/add_book.rs b/src/commands/add_book.rs index be46a6d..727d620 100644 --- a/src/commands/add_book.rs +++ b/src/commands/add_book.rs @@ -1,8 +1,7 @@ -use crate::i18n::tr; -use crate::isbn::normalize_isbn; +use crate::i18n::{tr, tr_with}; +use crate::utils::isbn::normalize_isbn; use crate::models::book::Book; -use crate::utils::lang_code_to_name; -use crate::{is_verbose, print_err, print_info, print_ok, print_warn, tr_with}; +use crate::utils::{is_verbose, lang_code_to_name, print_err, print_info, print_ok, print_warn}; use chrono::Utc; use reqwest::blocking::get; use rusqlite::{Connection, Error as RusqliteError, ErrorCode}; diff --git a/src/commands/db.rs b/src/commands/db.rs index 8e5dac5..da29bf5 100644 --- a/src/commands/db.rs +++ b/src/commands/db.rs @@ -1,5 +1,7 @@ +use crate::config::AppConfig; use crate::db::connection::ensure_schema; -use crate::{AppConfig, print_err, print_ok, print_warn, tr, tr_with}; +use crate::i18n::{tr, tr_with}; +use crate::utils::{print_err, print_ok, print_warn}; use rusqlite::Connection; use std::error::Error; use std::fs; diff --git a/src/commands/del_book.rs b/src/commands/del_book.rs index 8f079f7..008b020 100644 --- a/src/commands/del_book.rs +++ b/src/commands/del_book.rs @@ -1,4 +1,5 @@ -use crate::{print_err, print_info, print_ok, print_warn, tr_with, write_log}; +use crate::i18n::tr_with; +use crate::utils::{print_err, print_info, print_ok, print_warn, write_log}; use colored::*; use rusqlite::Connection; use std::io::{self, Write}; diff --git a/src/commands/edit_book.rs b/src/commands/edit_book.rs index b77694f..a4d8ea4 100644 --- a/src/commands/edit_book.rs +++ b/src/commands/edit_book.rs @@ -1,6 +1,7 @@ use crate::db::books::{get_book_fields, update_book_by_id, update_book_by_isbn}; use crate::cli::fields::EDITABLE_FIELDS; -use crate::{lang_code_to_name, print_err, print_info, print_ok, print_warn, tr, tr_with}; +use crate::i18n::{tr, tr_with}; +use crate::utils::{lang_code_to_name, print_err, print_info, print_ok, print_warn}; use rusqlite::Connection; use std::collections::HashMap; diff --git a/src/commands/import.rs b/src/commands/import.rs index 87192aa..0955072 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -1,6 +1,6 @@ use crate::i18n::tr_with; -use crate::utils::{is_verbose, print_ok}; -use crate::{Book, print_err}; +use crate::models::Book; +use crate::utils::{is_verbose, print_err, print_ok}; use csv::ReaderBuilder; use rusqlite::Connection; use std::io::BufReader; diff --git a/src/commands/list.rs b/src/commands/list.rs index e5daedc..b77c43a 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,7 +1,7 @@ use crate::models::book::Book; use crate::models::display::{BookFull, BookShort}; use crate::i18n::tr; -use crate::isbn::normalize_isbn; +use crate::utils::isbn::normalize_isbn; use crate::utils::{build_table, build_vertical_table, print_err}; use rusqlite::types::ToSql; use rusqlite::{Connection, Row}; diff --git a/src/commands/search_book.rs b/src/commands/search_book.rs index 88c5e1b..6bc1925 100644 --- a/src/commands/search_book.rs +++ b/src/commands/search_book.rs @@ -1,7 +1,7 @@ use crate::db::search_books; use crate::i18n::tr; use crate::models::{Book, BookFull, BookShort}; -use crate::print_warn; +use crate::utils::print_warn; use crate::utils::table::build_table; use rusqlite::Connection; use std::error::Error; diff --git a/src/lib.rs b/src/lib.rs index 9952b48..4a1f99b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,8 @@ //! Librius β€” a small library manager core crate //! //! This crate contains the core functionality used by the `librius` binary. -//! It is intentionally lightweight and exposes configuration helpers, the -//! database initialization routine and the primary domain model (`Book`). -//! -//! The binary (`src/main.rs`) uses this crate to perform startup and to -//! dispatch command handlers. Including a `lib.rs` makes this project -//! suitable for documentation generation on platforms such as docs.rs. +//! It exposes configuration helpers, the database initialization routine +//! and the primary domain model (`Book`). //! //! Example //! @@ -27,9 +23,16 @@ pub mod i18n; pub mod models; pub mod utils; -pub use commands::*; -pub use config::*; -pub use db::*; -pub use i18n::*; -pub use models::*; -pub use utils::*; +// --- Explicit public API re-exports --- + +// Configuration +pub use config::{load_or_init, AppConfig}; + +// Database +pub use db::{init_db, start_db}; + +// Domain model +pub use models::Book; + +// i18n (needed by internal modules and external callers) +pub use i18n::{load_language, tr, tr_s, tr_with}; diff --git a/src/main.rs b/src/main.rs index f71eccb..e90b626 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,14 +62,9 @@ fn main() { .unwrap_or_else(|_| panic!("{}", &tr_with("db.open.failed", &[("icon-err", ERR)]))); // ------------------------------------------------------------ - // 5️⃣ Esegue migrazioni DB e config + // 5️⃣ Esegue migrazioni config // ------------------------------------------------------------ - if let Err(e) = db::migrations::run_migrations(&conn) { - print_err(&tr_with("db.migrate.failed", &[("error", &e.to_string())])); - } else { - print_ok(&tr("db.schema.verified"), is_verbose()); - } - + // Note: db migrations are already run by start_db() above. if let Err(e) = config::migrate_config(&conn, &config::config_file_path()) { print_err(&tr_with( "config.migrate.failed", diff --git a/src/utils/isbn.rs b/src/utils/isbn.rs index 9118bb6..bdff10d 100644 --- a/src/utils/isbn.rs +++ b/src/utils/isbn.rs @@ -1,4 +1,4 @@ -use crate::tr_with; +use crate::i18n::tr_with; use isbn2::{Isbn, IsbnError}; use std::str::FromStr; @@ -11,7 +11,7 @@ use std::str::FromStr; /// /// # Examples /// ``` -/// # use librius::isbn::normalize_isbn; +/// # use librius::utils::isbn::normalize_isbn; /// /// let plain = normalize_isbn("978-88-203-8269-8", true).unwrap(); /// assert_eq!(plain, "9788820382698"); diff --git a/src/utils/table.rs b/src/utils/table.rs index 52229ff..36238f0 100644 --- a/src/utils/table.rs +++ b/src/utils/table.rs @@ -3,7 +3,8 @@ //! Provides a unified interface for rendering tabular data using the `tabled` crate, //! ensuring consistent visual style and alignment across commands. -use crate::{print_warn, tr}; +use crate::i18n::tr; +use crate::utils::print::print_warn; use serde::Serialize; use serde_json::Value; use tabled::settings::{Alignment, Modify, Style, object::Rows}; diff --git a/tests/isbn_tests.rs b/tests/isbn_tests.rs index 48f91f4..ff90c8f 100644 --- a/tests/isbn_tests.rs +++ b/tests/isbn_tests.rs @@ -1,4 +1,4 @@ -use librius::isbn::normalize_isbn; +use librius::utils::isbn::normalize_isbn; #[test] fn test_plain_output() { From 64ae0a79ebff697fd45394cb3a3f11906ba03fce Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 22:59:24 +0200 Subject: [PATCH 09/12] docs: add STRUCTURE.md with post-refactor module layout Documents the full src/ tree after the 6-phase refactor: - one-line description per file - key design rules table - explicit public API surface from lib.rs --- STRUCTURE.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 STRUCTURE.md diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..9033545 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,133 @@ +# Librius β€” Project Structure + +> Last updated after full refactor (v0.6.0). +> Each file has a single, well-defined responsibility. + +--- + +## Root layout + +``` +librius/ +β”œβ”€β”€ Cargo.toml # crate metadata and dependencies +β”œβ”€β”€ Cargo.lock +β”œβ”€β”€ build.rs # Windows resource embedding (winresource) +β”œβ”€β”€ README.md +β”œβ”€β”€ CHANGELOG.md +β”œβ”€β”€ STRUCTURE.md # this file +β”œβ”€β”€ LICENSE +β”œβ”€β”€ src/ # application source (see below) +β”œβ”€β”€ tests/ # integration tests +β”œβ”€β”€ res/ # Windows icon / resource files +β”œβ”€β”€ scripts/ # helper scripts (e.g. extract_translations.py) +β”œβ”€β”€ dev_tools/ # build-check and icon-generation scripts +└── tools/ # submodule-check helpers +``` + +--- + +## `src/` β€” Source tree + +``` +src/ +β”œβ”€β”€ main.rs # binary entry-point: parse args, load config, init DB, dispatch CLI +β”œβ”€β”€ lib.rs # library root: declares all modules, exposes explicit public API +β”‚ +β”œβ”€β”€ cli/ # CLI parsing and command dispatch +β”‚ β”œβ”€β”€ mod.rs # re-exports: build_cli, run_cli, EDITABLE_FIELDS +β”‚ β”œβ”€β”€ args.rs # builds the full clap::Command tree (localised with tr_s) +β”‚ β”œβ”€β”€ dispatch.rs # matches subcommands β†’ calls command handlers +β”‚ └── fields.rs # EDITABLE_FIELDS: list of book fields editable via CLI +β”‚ +β”œβ”€β”€ commands/ # one handler per user-facing command +β”‚ β”œβ”€β”€ mod.rs # re-exports all handle_* functions +β”‚ β”œβ”€β”€ add_book.rs # handle_add_book β€” fetches metadata via Google Books API +β”‚ β”œβ”€β”€ backup.rs # handle_backup β€” ZIP/tar database backup +β”‚ β”œβ”€β”€ config.rs # handle_config β€” init / print / edit config file +β”‚ β”œβ”€β”€ db.rs # handle_db β€” DB init, reset, copy +β”‚ β”œβ”€β”€ del_book.rs # handle_del_book β€” delete by ID or ISBN +β”‚ β”œβ”€β”€ edit_book.rs # handle_edit_book β€” update one or more fields +β”‚ β”œβ”€β”€ export.rs # handle_export_csv/xlsx/json +β”‚ β”œβ”€β”€ import.rs # handle_import_csv/json +β”‚ β”œβ”€β”€ list.rs # handle_list β€” tabular list with optional detail view +β”‚ └── search_book.rs # handle_search β€” full-text search across key fields +β”‚ +β”œβ”€β”€ config/ # application configuration (YAML) +β”‚ β”œβ”€β”€ mod.rs # re-exports: AppConfig, load_or_init, config_file_path, migrate_config +β”‚ β”œβ”€β”€ load_config.rs # AppConfig struct, YAML load/save, default path resolution +β”‚ └── migrate_config.rs # config schema migration (adds missing keys to existing files) +β”‚ +β”œβ”€β”€ db/ # SQLite database layer +β”‚ β”œβ”€β”€ mod.rs # re-exports: start_db, init_db, ensure_schema, run_migrations, +β”‚ β”‚ # search_books, get_book_fields, update_book_by_id/isbn +β”‚ β”œβ”€β”€ connection.rs # DB path resolution, connection open, schema init, migration dispatch +β”‚ β”œβ”€β”€ migrations.rs # incremental patch system (PATCH_001..N), MigrationResult enum +β”‚ └── books.rs # CRUD helpers: update_book_by_id/isbn, get_book_fields, search_books +β”‚ +β”œβ”€β”€ i18n/ # internationalisation +β”‚ β”œβ”€β”€ mod.rs # re-exports: load_language, tr, tr_s, tr_with, parse_json_to_map +β”‚ └── loader.rs # JSON translation loader, global language map, tr/tr_s/tr_with helpers +β”‚ +β”œβ”€β”€ models/ # domain models +β”‚ β”œβ”€β”€ mod.rs # re-exports: Book, BookFull, BookShort +β”‚ β”œβ”€β”€ book.rs # Book struct (pure data + Serde + from_row) β€” no i18n / tabled deps +β”‚ └── display.rs # BookFull, BookShort β€” Tabled wrappers with localised column headers +β”‚ +└── utils/ # generic utilities (one file per concern) + β”œβ”€β”€ mod.rs # aggregator: declares all submodules, explicit re-exports + β”œβ”€β”€ verbose.rs # VERBOSE global flag: set_verbose(), is_verbose() + β”œβ”€β”€ print.rs # icons module (OK/ERR/WARN/INFO) + print_ok/err/warn/info() + β”œβ”€β”€ log.rs # now_str(), write_log() β€” structured SQLite log entries + β”œβ”€β”€ import_helpers.rs # open_import_file(), handle_import_result() + β”œβ”€β”€ isbn.rs # normalize_isbn() β€” validation + hyphen formatting (isbn2 crate) + β”œβ”€β”€ lang.rs # lang_code_to_name() β€” ISO 639-1 code β†’ readable name + └── table.rs # build_table(), build_vertical_table() β€” tabled rendering helpers +``` + +--- + +## `tests/` β€” Integration tests + +``` +tests/ +β”œβ”€β”€ common.rs # shared test helpers (DB setup, temp paths) +β”œβ”€β”€ db_tests.rs # schema creation, insert + read round-trips +β”œβ”€β”€ isbn_tests.rs # normalize_isbn: plain, hyphenated, invalid inputs +└── librius_core_tests.rs # handle_list / handle_list --short end-to-end +``` + +--- + +## Key design rules (post-refactor) + +| Rule | Where enforced | +|------|---------------| +| **Single responsibility** | Each `.rs` file contains one cohesive concept | +| **No glob re-exports** | `lib.rs` only re-exports the explicit public API | +| **Explicit imports** | All internal modules use full `crate::x::y` paths β€” no shortcut from crate root | +| **No dead code** | Wrapper modules removed; duplicate functions eliminated | +| **Presentation β‰  data** | `models/book.rs` (data) vs `models/display.rs` (tabled + i18n) | +| **CLI concerns stay in `cli/`** | `EDITABLE_FIELDS` lives in `cli/fields.rs`, not `utils/` | +| **DB migrations run once** | `start_db()` (`db/connection.rs`) runs migrations; `main.rs` does not repeat them | + +--- + +## Public API surface (`lib.rs` re-exports) + +```rust +// config +pub use config::{AppConfig, load_or_init}; + +// db +pub use db::{init_db, start_db}; + +// models +pub use models::Book; + +// i18n +pub use i18n::{load_language, tr, tr_s, tr_with}; +``` + +All other symbols are accessible via their full module path (e.g. +`librius::utils::isbn::normalize_isbn`, `librius::commands::handle_list`). + From 81b573a77e1262be5ff199a99183d6e31a20413a Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 23:06:14 +0200 Subject: [PATCH 10/12] docs: update CHANGELOG, README and STRUCTURE for v0.6.0 refactor CHANGELOG.md: - Add [0.6.0] entry documenting all 6 refactor phases: utils/ split, cli/fields.rs, models/display.rs, db renames, commands cleanup, lib.rs explicit API, main.rs dedup README.md: - Replace 'New in v0.5.1' section with 'New in v0.6.0' refactor summary - Update Project structure tree (connection.rs, migrations.rs, display.rs, utils submodules, etc.) - Update Development notes: correct public API examples, remove stale glob-import example, add single-responsibility design rules - Remove already-implemented items from Future roadmap STRUCTURE.md: - Amend table formatting for GitHub Markdown rendering Cargo.toml: - Bump version 0.5.1 -> 0.6.0 Cargo.lock: - Regenerated after version bump --- CHANGELOG.md | 63 +++++ Cargo.lock | 633 ++++++++++++++++++++++++++++++++++++++------------- Cargo.toml | 30 +-- README.md | 139 ++++++----- STRUCTURE.md | 18 +- 5 files changed, 627 insertions(+), 256 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f26592c..8ff7922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,69 @@ All notable changes to this project will be documented in this file. +## [0.6.0] - 2026-04-27 + +### 🧱 Refactor β€” Complete modular restructuring + +This release introduces a full internal refactor of the codebase. No user-facing +behaviour is changed; all commands, flags and output remain identical. + +#### `utils/` β€” monolith broken into focused submodules + +- **`utils/verbose.rs`** β€” `VERBOSE` static, `set_verbose()`, `is_verbose()` +- **`utils/print.rs`** β€” `icons` module (`OK`/`ERR`/`WARN`/`INFO`) + `print_ok/err/warn/info()` +- **`utils/log.rs`** β€” `now_str()`, `write_log()` (structured SQLite log entries) +- **`utils/import_helpers.rs`** β€” `open_import_file()`, `handle_import_result()` +- **`utils/mod.rs`** β€” aggregator with explicit `pub use` only (no more glob re-exports) + +#### `cli/` β€” CLI concerns separated from utilities + +- **`cli/fields.rs`** β€” `EDITABLE_FIELDS` moved from `utils/` (it is a CLI concern, not a generic utility) +- **`cli/mod.rs`** β€” removed dead `Commands` enum (only 3 of 8 commands were represented, never used by dispatch) + +#### `models/` β€” data model separated from presentation + +- **`models/book.rs`** β€” pure data model: `Book` struct + `from_row()` + Serde only; no `tabled`/`i18n` dependencies +- **`models/display.rs`** *(new)* β€” `BookFull`, `BookShort` with `Tabled` implementations and localised column headers + +#### `db/` β€” module consolidation and cleaner naming + +- **`db/load_db.rs` β†’ `db/connection.rs`** β€” clearer name for DB connection management +- **`db/migrate_db.rs` β†’ `db/migrations.rs`** β€” clearer name for migration logic +- **`db/search.rs`** β€” absorbed into `db/books.rs`; `search_books()` belongs with book operations + +#### `commands/` β€” removal of dead code and duplications + +- **`commands/add.rs`** removed β€” 18-line wrapper that only called `handle_add_book()` directly (dead code) +- **`commands/list.rs`** β€” `row_to_book()` simplified to delegate to `Book::from_row()` + ISBN formatting only +- **`commands/db.rs`** β€” local `create_schema()` duplicate removed; delegates to `db::connection::ensure_schema()` +- **`commands/config.rs`** β€” `handle_config()` signature changed from `&Commands` enum to explicit bool parameters + `(init, print, edit, editor)` + +#### `lib.rs` β€” explicit public API + +- Removed all wildcard re-exports (`pub use commands::*`, `pub use utils::*`, etc.) +- Replaced with an explicit, minimal public API: + ```rust + pub use config::{AppConfig, load_or_init}; + pub use db::{init_db, start_db}; + pub use models::Book; + pub use i18n::{load_language, tr, tr_s, tr_with}; + ``` +- All internal modules now use full `crate::x::y` import paths + +#### `main.rs` β€” removed duplicate migration call + +- `run_migrations()` was called twice (once in `start_db()`, once explicitly in `main`). + Removed the redundant call in `main.rs`. + +### πŸ“„ Documentation + +- Added **`STRUCTURE.md`** β€” complete map of every source file with its single responsibility, + design rules table, and the explicit public API surface. + +--- + ## [0.5.1] - 2025-11-12 ### Added diff --git a/Cargo.lock b/Cargo.lock index 93574c4..a9d2780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,8 +15,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher 0.5.1", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] @@ -52,9 +63,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -73,9 +84,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -100,6 +111,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arbitrary" version = "1.4.2" @@ -117,9 +134,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -178,6 +195,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -237,7 +264,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -271,9 +298,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -289,15 +316,25 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.6", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common 0.2.1", + "inout 0.2.2", ] [[package]] name = "clap" -version = "4.5.51" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -305,9 +342,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -317,9 +354,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -329,9 +366,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "codegen" @@ -350,18 +393,24 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation" @@ -379,6 +428,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -389,20 +444,14 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.3.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "crc-catalog", + "libc", ] -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -428,6 +477,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -449,6 +507,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "deflate64" version = "0.1.10" @@ -487,11 +554,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", + "zeroize", +] + [[package]] name = "dirs" version = "6.0.0" @@ -604,13 +684,13 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -634,6 +714,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -737,8 +823,23 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", + "wasm-bindgen", ] [[package]] @@ -753,7 +854,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -772,22 +873,31 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -802,7 +912,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -860,6 +979,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.0" @@ -1045,6 +1173,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1084,12 +1218,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -1102,6 +1238,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1167,6 +1312,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -1192,7 +1343,7 @@ dependencies = [ [[package]] name = "librius" -version = "0.5.1" +version = "0.6.0" dependencies = [ "assert_cmd", "chrono", @@ -1213,29 +1364,20 @@ dependencies = [ "tar", "umya-spreadsheet", "winresource", - "zip 6.0.0", + "zip 8.6.0", ] [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1256,11 +1398,10 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lzma-rust2" -version = "0.13.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" dependencies = [ - "crc", "sha2", ] @@ -1271,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1332,9 +1473,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -1347,9 +1488,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1420,12 +1561,12 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest", - "hmac", + "digest 0.11.2", + "hmac 0.13.0", ] [[package]] @@ -1512,15 +1653,15 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -1546,6 +1687,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1570,9 +1721,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1589,9 +1740,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1602,6 +1753,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1716,11 +1873,21 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.17", +] + [[package]] name = "rusqlite" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags", "chrono", @@ -1729,6 +1896,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -1821,6 +1989,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1853,22 +2027,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -1891,7 +2065,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -1900,13 +2074,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -1916,8 +2090,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -1954,6 +2128,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1974,9 +2160,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2050,9 +2236,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -2141,22 +2327,23 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tinystr" @@ -2217,11 +2404,11 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime", @@ -2232,27 +2419,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -2324,11 +2511,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -2342,7 +2535,7 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "408c7e039c96ec1d517a1111ade7fadab889f32c096dac691a1e3b8018c3e39a" dependencies = [ - "aes", + "aes 0.8.4", "ahash", "base64", "byteorder", @@ -2352,7 +2545,7 @@ dependencies = [ "encoding_rs", "fancy-regex", "getrandom 0.2.16", - "hmac", + "hmac 0.12.1", "html_parser", "imagesize", "lazy_static", @@ -2377,6 +2570,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2465,7 +2664,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -2526,6 +2734,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.82" @@ -2639,15 +2881,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -2797,15 +3030,15 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "winresource" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ef04dd590e94ff7431a8eda99d5ca659e688d60e930bd0a330062acea4608f" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ "toml", "version_check", @@ -2817,6 +3050,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" @@ -2908,20 +3229,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "zerotrie" @@ -2967,7 +3274,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.12.0", + "indexmap 2.14.0", "memchr", "thiserror 2.0.17", "zopfli", @@ -2975,26 +3282,26 @@ dependencies = [ [[package]] name = "zip" -version = "6.0.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes", - "arbitrary", + "aes 0.9.0", "bzip2", "constant_time_eq", "crc32fast", "deflate64", "flate2", - "getrandom 0.3.4", - "hmac", - "indexmap 2.12.0", + "getrandom 0.4.2", + "hmac 0.13.0", + "indexmap 2.14.0", "lzma-rust2", "memchr", "pbkdf2", "ppmd-rust", "sha1", "time", + "typed-path", "zeroize", "zopfli", "zstd", @@ -3002,9 +3309,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 713e38b..87f6b23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librius" -version = "0.5.1" +version = "0.6.0" edition = "2024" authors = ["Alessandro Maestri "] description = "A personal library manager CLI written in Rust." @@ -18,17 +18,17 @@ identifier = "eu.umpire274.librius" icon = ["res/librius.png"] [dependencies] -clap = { version = "4.5.51", features = ["derive"] } +clap = { version = "4.6.1", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" -once_cell = "1.21.3" -chrono = { version = "0.4.42", features = ["serde"] } +serde_json = "1.0.149" +once_cell = "1.21.4" +chrono = { version = "0.4.44", features = ["serde"] } serde_yaml = "0.9.33" -rusqlite = { version = "0.37.0", features = ["bundled", "chrono"] } -colored = "3.0.0" -zip = { version = "6.0.0", optional = true } -flate2 = { version = "1.1.5", optional = true } -tar = { version = "0.4.44", optional = true } +rusqlite = { version = "0.39.0", features = ["bundled", "chrono"] } +colored = "3.1.1" +zip = { version = "8.6.0", optional = true } +flate2 = { version = "1.1.9", optional = true } +tar = { version = "0.4.45", optional = true } dirs = "6.0.0" umya-spreadsheet = "2.3.3" csv = "1.4.0" @@ -37,19 +37,19 @@ reqwest = { version = "0.12.24", features = ["blocking", "json"] } isbn2 = "0.4.0" [target.'cfg(windows)'.dependencies] -zip = "6.0.0" +zip = "8.6.0" [target.'cfg(not(windows))'.dependencies] flate2 = "1.1.4" tar = "0.4.44" [build-dependencies] -winresource = "0.1.27" +winresource = "0.1.31" [profile.release] opt-level = 3 [dev-dependencies] -assert_cmd = "2.1.1" -predicates = "3.1.3" -rusqlite = { version = "0.37.0", features = ["chrono"] } +assert_cmd = "2.2.1" +predicates = "3.1.4" +rusqlite = { version = "0.39.0", features = ["chrono"] } diff --git a/README.md b/README.md index 01fbe02..3376858 100644 --- a/README.md +++ b/README.md @@ -23,44 +23,27 @@ and import/export support. --- -### ✨ New in v0.5.1 +### ✨ New in v0.6.0 -**πŸ—„οΈ Database management command** +**🧱 Full internal refactor β€” cleaner module structure** -A brand new `db` command has been introduced for complete database lifecycle control: +This release performs a complete restructuring of the source tree without changing +any user-visible behaviour (commands, flags, output remain identical). -```bash -librius db --init -``` - -Initializes or resets the current database. - -```bash -librius db --reset -``` - -Alias of `--init`. - -```bash -librius db --copy -f|--file -``` - -Copies the database defined in your librius.yaml configuration to a new file. - -> The database path is automatically read from the database: key in the configuration file. +Key improvements: -**πŸ“š Improved list details view** +- `utils/` split into focused single-responsibility files: `verbose.rs`, `print.rs`, + `log.rs`, `import_helpers.rs` +- `models/book.rs` (pure data) separated from `models/display.rs` (Tabled + i18n presentation) +- `db/load_db.rs` β†’ `db/connection.rs` Β· `db/migrate_db.rs` β†’ `db/migrations.rs` Β· + `db/search.rs` absorbed into `db/books.rs` +- `cli/fields.rs` extracts `EDITABLE_FIELDS` from `utils/` where it did not belong +- Dead code removed: `commands/add.rs` wrapper, duplicate `create_schema()`, dead `Commands` enum +- `lib.rs` now exposes an explicit, minimal public API (no more glob re-exports) +- Duplicate `run_migrations()` call in `main.rs` eliminated +- Added [`STRUCTURE.md`](STRUCTURE.md) β€” full documented map of the source tree -- Added the -`-compact` flag for list `--id --details` to hide empty fields in the vertical table. -- The `--compact` flag now requires `--details`, ensuring consistent CLI behavior. -- Fixed table headers that previously displayed `String`; now they correctly show localized Field / Value columns. -- Field order in detailed view now matches the database schema (`id β†’ added_at`). - -**🐞 Fixes & improvements** - -- Fixed `--copy` flag incorrectly requiring a value. -- Improved integration between configuration and database operations. -- Enhanced localized messages and help text for better clarity. +> See [`CHANGELOG.md`](CHANGELOG.md) for the complete change log. --- @@ -347,45 +330,53 @@ tr_with!("db.path.open_existing", & [("path", & db_path)]); ## 🧱 Project structure +> Full details in [`STRUCTURE.md`](STRUCTURE.md). + ``` src/ -β”œβ”€ main.rs -β”œβ”€ lib.rs -β”œβ”€ cli.rs +β”œβ”€β”€ main.rs # binary entry-point +β”œβ”€β”€ lib.rs # library root β€” explicit public API +β”‚ +β”œβ”€β”€ cli/ +β”‚ β”œβ”€β”€ args.rs # clap command tree (localised) +β”‚ β”œβ”€β”€ dispatch.rs # subcommand β†’ handler routing +β”‚ β”œβ”€β”€ fields.rs # EDITABLE_FIELDS (CLI concern) +β”‚ └── mod.rs β”‚ -β”œβ”€ commands/ -β”‚ β”œβ”€ mod.rs -β”‚ β”œβ”€ list.rs -β”‚ β”œβ”€ backup.rs -β”‚ β”œβ”€ config.rs -β”‚ β”œβ”€ export.rs -β”‚ └─ import.rs +β”œβ”€β”€ commands/ # one handle_* function per command +β”‚ β”œβ”€β”€ add_book.rs Β· backup.rs Β· config.rs Β· db.rs +β”‚ β”œβ”€β”€ del_book.rs Β· edit_book.rs Β· export.rs +β”‚ β”œβ”€β”€ import.rs Β· list.rs Β· search_book.rs +β”‚ └── mod.rs β”‚ -β”œβ”€ config/ -β”‚ β”œβ”€ mod.rs -β”‚ β”œβ”€ load_config.rs -β”‚ └─ migrate_config.rs +β”œβ”€β”€ config/ +β”‚ β”œβ”€β”€ load_config.rs # AppConfig, YAML load/save +β”‚ β”œβ”€β”€ migrate_config.rs +β”‚ └── mod.rs β”‚ -β”œβ”€ db/ -β”‚ β”œβ”€ mod.rs -β”‚ β”œβ”€ load_db.rs -β”‚ └─ migrate_db.rs +β”œβ”€β”€ db/ +β”‚ β”œβ”€β”€ connection.rs # open / init / ensure_schema +β”‚ β”œβ”€β”€ migrations.rs # incremental patch system +β”‚ β”œβ”€β”€ books.rs # CRUD + search_books +β”‚ └── mod.rs β”‚ -β”œβ”€ i18n/ -β”‚ β”œβ”€ mod.rs -β”‚ β”œβ”€ loader.rs -β”‚ └─ locales/ -β”‚ β”œβ”€ en.json -β”‚ β”œβ”€ it.json -β”‚ └─ README.md +β”œβ”€β”€ i18n/ +β”‚ β”œβ”€β”€ loader.rs # tr / tr_s / tr_with +β”‚ β”œβ”€β”€ mod.rs +β”‚ └── locales/ # en.json Β· it.json β”‚ -β”œβ”€ models/ -β”‚ β”œβ”€ mod.rs -β”‚ └─ book.rs +β”œβ”€β”€ models/ +β”‚ β”œβ”€β”€ book.rs # Book struct β€” pure data + Serde +β”‚ β”œβ”€β”€ display.rs # BookFull / BookShort (Tabled + i18n) +β”‚ └── mod.rs β”‚ -└─ utils/ - β”œβ”€ mod.rs - └─ table.rs +└── utils/ + β”œβ”€β”€ verbose.rs # set_verbose / is_verbose + β”œβ”€β”€ print.rs # icons + print_ok/err/warn/info + β”œβ”€β”€ log.rs # write_log / now_str + β”œβ”€β”€ import_helpers.rs + β”œβ”€β”€ isbn.rs Β· lang.rs Β· table.rs + └── mod.rs ``` --- @@ -490,16 +481,21 @@ language: "en" ## 🧩 Development notes -Librius now follows a standard Rust modular structure: +Librius follows a strict single-responsibility module structure: -- Each domain (commands, db, config, models, utils, i18n) exposes its API via mod.rs. -- Common utilities like build_table() are reused across commands for consistent output. -- The lib.rs re-exports all major modules for cleaner imports in main.rs. +- Each `.rs` file has one cohesive concern (data, display, log, print, …). +- `lib.rs` exposes only an explicit, minimal public API β€” no wildcard re-exports. +- Internal modules always use full `crate::x::y` paths; no implicit crate-root shortcuts. +- DB migrations run exactly once, inside `db::connection::start_db()`. -### Example import +### Public API (lib.rs) ```rust -use librius::{build_cli, handle_list, tr}; +use librius::config::AppConfig; +use librius::db; +use librius::commands::handle_list; +use librius::utils::isbn::normalize_isbn; +use librius::i18n::tr; ``` --- @@ -614,10 +610,9 @@ cargo clippy ## 🧱 Future roadmap -- Add `add`, `remove`, and `search` commands -- Export/import JSON and CSV - Add optional TUI (Text UI) with `ratatui` - Web dashboard sync +- `docs.rs` full documentation coverage --- diff --git a/STRUCTURE.md b/STRUCTURE.md index 9033545..bee9bb2 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -100,15 +100,15 @@ tests/ ## Key design rules (post-refactor) -| Rule | Where enforced | -|------|---------------| -| **Single responsibility** | Each `.rs` file contains one cohesive concept | -| **No glob re-exports** | `lib.rs` only re-exports the explicit public API | -| **Explicit imports** | All internal modules use full `crate::x::y` paths β€” no shortcut from crate root | -| **No dead code** | Wrapper modules removed; duplicate functions eliminated | -| **Presentation β‰  data** | `models/book.rs` (data) vs `models/display.rs` (tabled + i18n) | -| **CLI concerns stay in `cli/`** | `EDITABLE_FIELDS` lives in `cli/fields.rs`, not `utils/` | -| **DB migrations run once** | `start_db()` (`db/connection.rs`) runs migrations; `main.rs` does not repeat them | +| Rule | Where enforced | +|---------------------------------|-----------------------------------------------------------------------------------| +| **Single responsibility** | Each `.rs` file contains one cohesive concept | +| **No glob re-exports** | `lib.rs` only re-exports the explicit public API | +| **Explicit imports** | All internal modules use full `crate::x::y` paths β€” no shortcut from crate root | +| **No dead code** | Wrapper modules removed; duplicate functions eliminated | +| **Presentation β‰  data** | `models/book.rs` (data) vs `models/display.rs` (tabled + i18n) | +| **CLI concerns stay in `cli/`** | `EDITABLE_FIELDS` lives in `cli/fields.rs`, not `utils/` | +| **DB migrations run once** | `start_db()` (`db/connection.rs`) runs migrations; `main.rs` does not repeat them | --- From aeddd94e2889ea0810ea892343b199da7df15548 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 23:29:08 +0200 Subject: [PATCH 11/12] fix(fmt): fixed error during 'cargo fmt --all' check --- src/cli/fields.rs | 1 - src/commands/add_book.rs | 2 +- src/commands/config.rs | 4 +--- src/commands/edit_book.rs | 2 +- src/commands/list.rs | 2 +- src/db/connection.rs | 1 - src/lib.rs | 2 +- src/models/display.rs | 1 - src/utils/import_helpers.rs | 1 - src/utils/log.rs | 1 - src/utils/print.rs | 1 - src/utils/verbose.rs | 1 - 12 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/cli/fields.rs b/src/cli/fields.rs index 1015c91..d55eda0 100644 --- a/src/cli/fields.rs +++ b/src/cli/fields.rs @@ -23,4 +23,3 @@ pub const EDITABLE_FIELDS: &[(&str, &str, char)] = &[ ("row", "help.edit.book.row", 'w'), ("position", "help.edit.book.position", 'o'), ]; - diff --git a/src/commands/add_book.rs b/src/commands/add_book.rs index 727d620..28114da 100644 --- a/src/commands/add_book.rs +++ b/src/commands/add_book.rs @@ -1,6 +1,6 @@ use crate::i18n::{tr, tr_with}; -use crate::utils::isbn::normalize_isbn; use crate::models::book::Book; +use crate::utils::isbn::normalize_isbn; use crate::utils::{is_verbose, lang_code_to_name, print_err, print_info, print_ok, print_warn}; use chrono::Utc; use reqwest::blocking::get; diff --git a/src/commands/config.rs b/src/commands/config.rs index 618ee1e..bfaed83 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -22,9 +22,7 @@ pub fn handle_config( print_info(&tr("config.schema.list"), true); println!("\n{}", contents); } - Err(e) => { - print_err(&tr_with("config.open.failed", &[("error", &e.to_string())])) - } + Err(e) => print_err(&tr_with("config.open.failed", &[("error", &e.to_string())])), } } else { print_warn(&tr("config.file.not_found")); diff --git a/src/commands/edit_book.rs b/src/commands/edit_book.rs index a4d8ea4..8e4ac9d 100644 --- a/src/commands/edit_book.rs +++ b/src/commands/edit_book.rs @@ -1,5 +1,5 @@ -use crate::db::books::{get_book_fields, update_book_by_id, update_book_by_isbn}; use crate::cli::fields::EDITABLE_FIELDS; +use crate::db::books::{get_book_fields, update_book_by_id, update_book_by_isbn}; use crate::i18n::{tr, tr_with}; use crate::utils::{lang_code_to_name, print_err, print_info, print_ok, print_warn}; use rusqlite::Connection; diff --git a/src/commands/list.rs b/src/commands/list.rs index b77c43a..346e458 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,6 +1,6 @@ +use crate::i18n::tr; use crate::models::book::Book; use crate::models::display::{BookFull, BookShort}; -use crate::i18n::tr; use crate::utils::isbn::normalize_isbn; use crate::utils::{build_table, build_vertical_table, print_err}; use rusqlite::types::ToSql; diff --git a/src/db/connection.rs b/src/db/connection.rs index 025944b..972b3db 100644 --- a/src/db/connection.rs +++ b/src/db/connection.rs @@ -149,4 +149,3 @@ pub fn ensure_schema(conn: &Connection) -> Result<()> { )?; Ok(()) } - diff --git a/src/lib.rs b/src/lib.rs index 4a1f99b..bd69025 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ pub mod utils; // --- Explicit public API re-exports --- // Configuration -pub use config::{load_or_init, AppConfig}; +pub use config::{AppConfig, load_or_init}; // Database pub use db::{init_db, start_db}; diff --git a/src/models/display.rs b/src/models/display.rs index fda81a3..e7e538c 100644 --- a/src/models/display.rs +++ b/src/models/display.rs @@ -78,4 +78,3 @@ impl<'a> Tabled for BookShort<'a> { ] } } - diff --git a/src/utils/import_helpers.rs b/src/utils/import_helpers.rs index 00455fa..c21c220 100644 --- a/src/utils/import_helpers.rs +++ b/src/utils/import_helpers.rs @@ -44,4 +44,3 @@ pub fn handle_import_result( } } } - diff --git a/src/utils/log.rs b/src/utils/log.rs index 64049f7..dd91d29 100644 --- a/src/utils/log.rs +++ b/src/utils/log.rs @@ -56,4 +56,3 @@ pub fn write_log(conn: &Connection, operation: &str, target: &str, message: &str Ok(()) } - diff --git a/src/utils/print.rs b/src/utils/print.rs index 3f09c6c..d090b51 100644 --- a/src/utils/print.rs +++ b/src/utils/print.rs @@ -42,4 +42,3 @@ pub fn print_info(msg: &str, verbose: bool) { } println!("{}{}", icons::INFO, msg.blue().bold()); } - diff --git a/src/utils/verbose.rs b/src/utils/verbose.rs index 6e43255..8b3a6cf 100644 --- a/src/utils/verbose.rs +++ b/src/utils/verbose.rs @@ -17,4 +17,3 @@ pub fn set_verbose(enabled: bool) { pub fn is_verbose() -> bool { *VERBOSE.get().unwrap_or(&false) } - From 0382b32f92a13bad452bc13e2057c834fc6f842b Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 27 Apr 2026 23:35:06 +0200 Subject: [PATCH 12/12] =?UTF-8?q?fix(models):=20parse=20non-RFC3339=20SQLi?= =?UTF-8?q?te=20timestamps=20in=20Book::from=5Frow=20Closes=20#21=20SQLite?= =?UTF-8?q?=20CURRENT=5FTIMESTAMP=20writes=20'YYYY-MM-DD=20HH:MM:SS'=20(no?= =?UTF-8?q?=20timezone),=20but=20rusqlite's=20built-in=20DateTime=20d?= =?UTF-8?q?eserialization=20expects=20RFC3339.=20This=20caused=20a=20row?= =?UTF-8?q?=20conversion=20error=20on=20any=20database=20populated=20via?= =?UTF-8?q?=20the=20default=20schema,=20making=20'librius=20list'=20fail?= =?UTF-8?q?=20instead=20of=20rendering=20results.=20Fix:=20read=20added=5F?= =?UTF-8?q?at=20as=20Option=20and=20parse=20with=20a=20multi-forma?= =?UTF-8?q?t=20fallback=20function=20(parse=5Fsqlite=5Fdatetime),=20gracef?= =?UTF-8?q?ully=20degrading=20to=20None=20if=20no=20format=20matches=20?= =?UTF-8?q?=E2=80=94=20identical=20behaviour=20to=20the=20pre-refactor=20m?= =?UTF-8?q?apper.=20Formats=20handled:=20=20=201.=20RFC3339=20with=20timez?= =?UTF-8?q?one=20=20(2025-10-13T21:32:07+02:00)=20=20=202.=20SQLite=20CURR?= =?UTF-8?q?ENT=5FTIMESTAMP=20=20(2025-10-13=2021:32:07)=20=20=203.=20SQLit?= =?UTF-8?q?e=20CURRENT=5FTIMESTAMP=20with=20sub-seconds=20=20(2025-10-13?= =?UTF-8?q?=2021:32:07.123)=20Tests=20added=20(tests/db=5Ftests.rs):=20=20?= =?UTF-8?q?=20-=20test=5Ffrom=5Frow=5Fparses=5Fsqlite=5Fcurrent=5Ftimestam?= =?UTF-8?q?p=20=20(regression=20case)=20=20=20-=20test=5Ffrom=5Frow=5Fpars?= =?UTF-8?q?es=5Frfc3339=5Ftimestamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/book.rs | 35 ++++++++++++++++++++++-- tests/db_tests.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/models/book.rs b/src/models/book.rs index a0da5ee..8b6ee8b 100644 --- a/src/models/book.rs +++ b/src/models/book.rs @@ -6,7 +6,7 @@ // serializzazione e costruzione da riga SQLite. // ===================================================== -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use rusqlite::Row; use serde::{Deserialize, Serialize}; @@ -31,6 +31,15 @@ pub struct Book { impl Book { pub fn from_row(row: &Row) -> rusqlite::Result { + // `added_at` is stored by SQLite as either: + // - RFC3339 / ISO 8601 with timezone: "2025-10-13T21:32:07+02:00" + // - SQLite CURRENT_TIMESTAMP (no timezone): "2025-10-13 21:32:07" + // - SQLite CURRENT_TIMESTAMP with sub-seconds: "2025-10-13 21:32:07.123" + // rusqlite's built-in DateTime conversion only accepts RFC3339, + // so we read as String and parse manually, degrading to None on failure. + let added_at_str: Option = row.get("added_at")?; + let added_at = added_at_str.as_deref().and_then(parse_sqlite_datetime); + Ok(Self { id: row.get("id")?, title: row.get("title")?, @@ -46,7 +55,29 @@ impl Book { shelf: row.get("shelf")?, row: row.get("row")?, position: row.get("position")?, - added_at: row.get("added_at")?, + added_at, }) } } + +/// Parse a SQLite timestamp string into `DateTime`. +/// +/// Tries the following formats in order, returning `None` if none match: +/// 1. RFC3339 / ISO 8601 with timezone (`2025-10-13T21:32:07+02:00`) +/// 2. SQLite `CURRENT_TIMESTAMP` (`2025-10-13 21:32:07`) +/// 3. SQLite with sub-second precision (`2025-10-13 21:32:07.123`) +fn parse_sqlite_datetime(s: &str) -> Option> { + // 1. RFC3339 (written by chrono or explicit INSERT) + if let Ok(dt) = DateTime::parse_from_rfc3339(s) { + return Some(dt.with_timezone(&Utc)); + } + // 2. SQLite CURRENT_TIMESTAMP: "YYYY-MM-DD HH:MM:SS" + if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") { + return Some(DateTime::from_naive_utc_and_offset(naive, Utc)); + } + // 3. SQLite CURRENT_TIMESTAMP with fractional seconds + if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") { + return Some(DateTime::from_naive_utc_and_offset(naive, Utc)); + } + None +} diff --git a/tests/db_tests.rs b/tests/db_tests.rs index 5d82b38..ea17d4a 100644 --- a/tests/db_tests.rs +++ b/tests/db_tests.rs @@ -58,3 +58,71 @@ fn test_insert_and_read_book_full_schema() { assert_eq!(row.1, "Umberto Eco"); assert_eq!(row.2, "Romanzo storico"); } + +#[test] +fn test_from_row_parses_sqlite_current_timestamp() { + // Regression test for: Book::from_row must not fail when added_at is stored + // as SQLite CURRENT_TIMESTAMP format ("YYYY-MM-DD HH:MM:SS") instead of RFC3339. + use librius::models::Book; + + let conn = setup_temp_db("timestamp_regression"); + + // Insert with SQLite CURRENT_TIMESTAMP (produces "YYYY-MM-DD HH:MM:SS") + conn.execute( + "INSERT INTO books (title, author, editor, year, isbn, added_at) + VALUES ('Dune', 'Frank Herbert', 'Chilton', 1965, '9780441013593', datetime('now'))", + [], + ) + .unwrap(); + + // Book::from_row must succeed and either parse the date or degrade to None β€” never error. + let mut stmt = conn + .prepare( + "SELECT id, title, author, editor, year, isbn, language, pages, + genre, summary, room, shelf, row, position, added_at + FROM books WHERE isbn = '9780441013593'", + ) + .unwrap(); + + let book = stmt + .query_row([], Book::from_row) + .expect("from_row must not fail on SQLite CURRENT_TIMESTAMP format"); + + assert_eq!(book.title, "Dune"); + // added_at should be Some (parsed successfully), not None and not an error + assert!( + book.added_at.is_some(), + "added_at should be parsed from SQLite timestamp format" + ); +} + +#[test] +fn test_from_row_parses_rfc3339_timestamp() { + use librius::models::Book; + + let conn = setup_temp_db("timestamp_rfc3339"); + + // Insert with explicit RFC3339 timestamp (written by chrono) + conn.execute( + "INSERT INTO books (title, author, editor, year, isbn, added_at) + VALUES ('Foundation', 'Isaac Asimov', 'Gnome Press', 1951, '9780553293357', + '2025-10-13T21:32:07+00:00')", + [], + ) + .unwrap(); + + let mut stmt = conn + .prepare( + "SELECT id, title, author, editor, year, isbn, language, pages, + genre, summary, room, shelf, row, position, added_at + FROM books WHERE isbn = '9780553293357'", + ) + .unwrap(); + + let book = stmt + .query_row([], Book::from_row) + .expect("from_row must not fail on RFC3339 timestamp"); + + assert_eq!(book.title, "Foundation"); + assert!(book.added_at.is_some(), "RFC3339 timestamp must be parsed"); +}