From 12a35fe4cc0e72426e842481cafcce33fee40006 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Sat, 18 Oct 2025 00:33:48 +0200 Subject: [PATCH 1/4] feat(commands): add 'librius add book' command with Google Books API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented new subcommand add book --isbn for automatic metadata retrieval. Integrated Google Books API to fetch title, author, publisher, year, and language. Added ISO 639-1 β†’ full language name conversion utility. Localized all messages and help texts (English / Italian). Refactored Default trait derivations for cleaner structs. Improved error handling and field mapping for API responses. --- CHANGELOG.md | 36 ++ Cargo.lock | 1039 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- README.md | 34 +- src/cli.rs | 20 + src/commands/add.rs | 17 + src/commands/add_book.rs | 124 +++++ src/commands/mod.rs | 4 + src/i18n/locales/en.json | 15 +- src/i18n/locales/it.json | 15 +- src/utils/lang.rs | 20 + src/utils/mod.rs | 2 + 12 files changed, 1293 insertions(+), 36 deletions(-) create mode 100644 src/commands/add.rs create mode 100644 src/commands/add_book.rs create mode 100644 src/utils/lang.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f71dd0..d848b62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this project will be documented in this file. +## [0.4.0] - 2025-10-18 + +### Added + +- New command `add book`: + - Fetches book information automatically from the Google Books API using ISBN. + - Populates title, author, editor, year, language, genre, and summary automatically. + - Fallback to interactive mode (planned) for books not found. +- Integrated dynamic i18n support for all CLI help messages (`add`, `book`, `isbn`). +- Added automatic language name resolution (e.g., `"it"` β†’ `"Italian"`). +- New utility module `utils/lang.rs` for ISO 639-1 to language name conversion. +- Localized console messages for book lookup and insertion results. + +### Changed + +- Modularized command structure: added `add.rs` and `add_book.rs` under `src/commands/`. +- Improved error handling for Google Books API responses and JSON decoding. +- Replaced manual `impl Default` blocks with idiomatic `#[derive(Default)]`. + +### Fixed + +- Deserialization issues with Google Books API fields (`volumeInfo`, `publishedDate`, `pageCount`). +- Empty fields on insertion caused by incorrect field mapping. + +### Example usage + +```bash +$ librius add book --isbn 9788820382698 +πŸ” Ricerca del libro con ISBN: 9788820382698 +πŸ“˜ Libro trovato: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) +βœ… Libro β€œLa lingua dell'antico Egitto” aggiunto con successo. + +``` + +--- + ## [0.3.5] - 2025-10-17 ### Added diff --git a/Cargo.lock b/Cargo.lock index cf29255..2ba2440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -178,6 +184,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "bzip2" version = "0.6.0" @@ -236,7 +248,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -310,6 +322,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -509,6 +531,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" version = "0.2.26" @@ -550,6 +578,80 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -583,6 +685,25 @@ dependencies = [ "wasip2", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -637,6 +758,126 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -661,6 +902,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "imagesize" version = "0.14.0" @@ -687,6 +1035,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -750,7 +1114,7 @@ dependencies = [ [[package]] name = "librius" -version = "0.3.5" +version = "0.4.0" dependencies = [ "chrono", "clap", @@ -759,6 +1123,7 @@ dependencies = [ "dirs", "flate2", "once_cell", + "reqwest", "rusqlite", "serde", "serde_json", @@ -795,6 +1160,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "log" version = "0.4.28" @@ -827,6 +1198,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -837,6 +1214,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -864,6 +1269,50 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -891,6 +1340,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.3" @@ -934,12 +1389,33 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1058,17 +1534,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] -name = "rusqlite" -version = "0.37.0" +name = "reqwest" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -1084,6 +1616,39 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1096,6 +1661,38 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1139,6 +1736,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1186,12 +1795,34 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -1215,6 +1846,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tabled" version = "0.20.0" @@ -1250,6 +1922,19 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + [[package]] name = "testing_table" version = "0.3.0" @@ -1330,6 +2015,133 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -1389,6 +2201,30 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1417,6 +2253,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1459,6 +2304,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.104" @@ -1491,6 +2349,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1499,9 +2367,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -1526,19 +2394,54 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -1547,7 +2450,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1568,6 +2480,15 @@ dependencies = [ "windows-targets 0.53.5", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1590,7 +2511,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -1703,6 +2624,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xattr" version = "1.6.1" @@ -1713,6 +2640,30 @@ dependencies = [ "rustix", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -1733,6 +2684,27 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" @@ -1753,6 +2725,39 @@ dependencies = [ "syn", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zip" version = "2.4.2" diff --git a/Cargo.toml b/Cargo.toml index f21e54d..dfa6151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librius" -version = "0.3.5" +version = "0.4.0" edition = "2024" authors = ["Alessandro Maestri "] description = "A personal library manager CLI written in Rust." @@ -42,6 +42,7 @@ dirs = "6.0.0" umya-spreadsheet = "2.3.3" csv = "1.3.1" tabled = "0.20.0" +reqwest = { version = "0.12.24", features = ["blocking", "json"] } [target.'cfg(windows)'.dependencies] zip = "6.0.0" diff --git a/README.md b/README.md index 817bf2d..c0f652d 100644 --- a/README.md +++ b/README.md @@ -23,28 +23,30 @@ and import/export support. --- -## ✨ New in v0.3.0 +## ✨ New in v0.4.0 -**πŸ†• Modern tabular output** +**πŸ“š Automatic Book Fetching via Google Books API** -- Replaced the old `println!` list format with the [`tabled`](https://crates.io/crates/tabled) crate. -- Tables now feature aligned, styled columns for improved readability. -- Added a `--short` flag for compact view (`ID`, `Title`, `Author`, `Editor`, `Year`). -- Added `--id` and `--details` options to view a single record: - - `--id ` shows a specific book by its ID. - - `--details` displays all fields of the selected record in a vertical table. +- Introduced the new command `librius add book --isbn `. +- Automatically retrieves metadata from the Google Books API: + - Title, Author(s), Publisher, Year, Language, Category, Summary. +- The command inserts the record directly into your local database. +- If the book is not found, Librius will later support interactive entry mode. -**🧩 Modular architecture** +**🌍 Localized Help and Language Mapping** -- Standardized all modules using the `mod.rs` structure. -- Each subsystem (`commands`, `models`, `utils`, `db`, `config`, `i18n`) now has a clean, isolated namespace. -- Simplified imports using `pub use` re-exports in `lib.rs`. +- All help messages are dynamically localized (English / Italian). +- Language codes like `"it"` are automatically expanded to `"Italian"`. +- Improved JSON decoding and error reporting for external API calls. -**🧱 Utility improvements** +Example: -- Added a reusable `build_table()` helper in `utils/table.rs` for consistent table rendering. -- Introduced a dynamic `build_vertical_table()` helper for full record details using `serde_json` + `tabled`. -- Implemented `BookFull` and `BookShort` structs implementing `Tabled` for both full and compact listings. +```bash +$ librius add book --isbn 9788820382698 +πŸ” Searching for book with ISBN: 9788820382698 +πŸ“˜ Book found: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) +βœ… Book β€œLa lingua dell'antico Egitto” added successfully. +``` --- diff --git a/src/cli.rs b/src/cli.rs index 9f4b7fd..91897b5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -167,6 +167,17 @@ pub fn build_cli() -> Command { .value_parser(clap::builder::NonEmptyStringValueParser::new()), ), ) + .subcommand( + Command::new("add").about(tr("help.add.about")).subcommand( + Command::new("book").about(tr("help.add.book.about")).arg( + Arg::new("isbn") + .long("isbn") + .help(tr("help.add.book.isbn")) + .required(true) + .value_name("ISBN"), + ), + ), + ) // help come subcommand dedicato (es: `librius help config`) .subcommand( Command::new("help").about(tr_s("help_flag_about")).arg( @@ -262,6 +273,15 @@ pub fn run_cli( )); } + Ok(()) + } else if let Some(("add", sub_m)) = matches.subcommand() { + if let Some(("book", book_m)) = sub_m.subcommand() { + if let Some(isbn) = book_m.get_one::("isbn") { + crate::commands::handle_add_book(conn, isbn)?; + } else { + print_err(&tr("help.add.book.isbn")); + } + } Ok(()) } else if let Some(("help", sub_m)) = matches.subcommand() { if let Some(cmd_name) = sub_m.get_one::("command") { diff --git a/src/commands/add.rs b/src/commands/add.rs new file mode 100644 index 0000000..e698ff7 --- /dev/null +++ b/src/commands/add.rs @@ -0,0 +1,17 @@ +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/add_book.rs b/src/commands/add_book.rs new file mode 100644 index 0000000..ee2948f --- /dev/null +++ b/src/commands/add_book.rs @@ -0,0 +1,124 @@ +use crate::i18n::tr; +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 chrono::Utc; +use reqwest::blocking::get; +use rusqlite::Connection; +use serde::Deserialize; +use std::error::Error; + +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +struct GoogleBooksResponse { + items: Option>, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +struct GoogleBookItem { + #[serde(rename = "volumeInfo")] + volume_info: VolumeInfo, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +struct VolumeInfo { + title: Option, + authors: Option>, + publisher: Option, + published_date: Option, + description: Option, + page_count: Option, + language: Option, + categories: Option>, +} + +pub fn handle_add_book(conn: &Connection, isbn: &str) -> Result<(), Box> { + println!("\n{} {}", tr("add.lookup"), isbn); + + let url = format!("https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn}"); + let resp = get(&url)?; + + if !resp.status().is_success() { + print_err(&tr_with( + "book.add.http_error", + &[("status", &resp.status().to_string())], + )); + return Ok(()); + } + + let text = resp.text()?; + let response: Result = serde_json::from_str(&text); + + match response { + Ok(data) => { + if let Some(items) = data.items { + let info = &items[0].volume_info; + + // 🧩 Debug temporaneo per vedere i dati ricevuti + let debug_info = format!("{:#?}", info); + print_info( + &tr_with("book.add.book_info", &[("info", &debug_info)]), + is_verbose(), + ); + + let new_book = Book { + id: Some(0), + title: info.title.clone().unwrap_or_default(), + author: info + .authors + .as_ref() + .map(|a| a.join(", ")) + .unwrap_or_default(), + editor: info.publisher.clone().unwrap_or_default(), + year: info + .published_date + .as_ref() + .and_then(|d| d.get(0..4)) + .and_then(|y| y.parse::().ok()) + .unwrap_or_default(), + isbn: isbn.to_string(), + language: info + .language + .as_ref() + .map(|c| lang_code_to_name(c).to_string()), + pages: info.page_count, + genre: info.categories.as_ref().map(|c| c.join(", ")), + summary: info.description.clone(), + room: None, + shelf: None, + row: None, + position: None, + added_at: Some(Utc::now()), + }; + + conn.execute( + "INSERT INTO books (title, author, editor, year, isbn, language, pages, genre, summary, added_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, CURRENT_TIMESTAMP)", + rusqlite::params![ + new_book.title, + new_book.author, + new_book.editor, + new_book.year, + new_book.isbn, + new_book.language, + new_book.pages, + new_book.genre, + new_book.summary, + ], + )?; + + print_ok(&tr_with("add.success", &[("title", &new_book.title)]), true); + } else { + print_warn(&tr("add.no_result")); + } + } + Err(e) => { + print_err(&tr_with("add.decode_error", &[("error", &e.to_string())])); + // eprintln!("Raw JSON:\n{}", text); + } + } + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e953749..3534a55 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,12 +4,16 @@ //! `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; pub mod export; pub mod import; pub mod list; +pub use add::handle_add; +pub use add_book::handle_add_book; pub use backup::handle_backup; pub use config::handle_config; pub use export::handle_export_csv; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0590c58..8a9414a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -110,5 +110,18 @@ "import.error.unexpected": "Unexpected error during import: {error}", "help.list.details": "Show all fields of the specified record (requires --id)", "list.error.details_requires_id": "The --details flag can only be used together with --id .", - "help.list.id": "Specify the record ID to show" + "help.list.id": "Specify the record ID to show", + "help.add.about": "Add a new item to your library", + "help.add.book.about": "Add a new book by ISBN using Google Books API", + "help.add.book.isbn": "ISBN of the book to fetch", + "help.add.usage": "Use: librius add book --isbn ", + "add.lookup": "πŸ” Looking up book with ISBN:", + "add.success": "Book '{title}' added successfully.", + "add.no_result": "No book found for the provided ISBN.", + "help.app.about": "Manage your personal library with Librius CLI", + "help.list.about": "List all books in your collection", + "help.unknown_command": "Unknown command. Use --help to see available options.", + "book.add.http_error": "HTTP error while fetching book data: {error}", + "book.add.book_info": "Parsed book info:\n{info}\n", + "add.decode_error": "Error decoding book data: {error}" } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 22dfe7a..a41ecc7 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -110,5 +110,18 @@ "import.error.unexpected": "Errore imprevisto durante l'importazione: {error}", "help.list.id": "Specifica l'ID del record da visualizzare", "help.list.details": "Mostra tutti i campi del record specificato (richiede --id)", - "list.error.details_requires_id": "Il flag --details puΓ² essere usato solo insieme a --id ." + "list.error.details_requires_id": "Il flag --details puΓ² essere usato solo insieme a --id .", + "help.add.about": "Aggiunge un nuovo elemento alla libreria", + "help.add.book.about": "Aggiunge un libro tramite ISBN usando l'API di Google Books", + "help.add.book.isbn": "ISBN del libro da cercare", + "help.add.usage": "Uso: librius add book --isbn ", + "add.lookup": "πŸ” Ricerca del libro con ISBN:", + "add.success": "Libro '{title}' aggiunto con successo.", + "add.no_result": "Nessun libro trovato per l'ISBN fornito.", + "help.app.about": "Gestisci la tua biblioteca personale con Librius CLI", + "help.list.about": "Elenco tutti i libri nella tua collezione", + "help.unknown_command": "Comando sconosciuto. Usa --help per vedere le opzioni disponibili.", + "book.add.http_error": "Errore HTTP durante il recupero dei dati del libro: {error}", + "book.add.book_info": "Informazioni sul libro analizzate:\n{info}\n", + "add.decode_error": "Errore durante la decodifica dei dati del libro: {error}" } diff --git a/src/utils/lang.rs b/src/utils/lang.rs new file mode 100644 index 0000000..9476f27 --- /dev/null +++ b/src/utils/lang.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +/// Converts a Google Books language code (ISO 639-1) into a readable name. +pub fn lang_code_to_name(code: &str) -> &str { + let map = HashMap::from([ + ("en", "English"), + ("it", "Italian"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ("pt", "Portuguese"), + ("ru", "Russian"), + ("zh", "Chinese"), + ("ja", "Japanese"), + ("ar", "Arabic"), + ("el", "Greek"), + ("la", "Latin"), + ]); + map.get(code).copied().unwrap_or(code) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3ed1fb1..62bd63e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,8 +4,10 @@ // Contiene funzioni di supporto generali e costanti // grafiche per output CLI. // ===================================================== +pub mod lang; pub mod table; +pub use lang::lang_code_to_name; pub use table::{build_table, build_vertical_table}; use crate::i18n::tr_with; From eb63aa1c337e8a5b1ba268e220a36c76b9974781 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 20 Oct 2025 10:53:04 +0200 Subject: [PATCH 2/4] feat(utils): add normalize_isbn() helper for validation and formatted output - Introduced new utility module utils/isbn.rs with normalize_isbn() function. - Added full ISBN-10 and ISBN-13 validation with bidirectional conversion. - Implemented localized error messages via i18n for invalid ISBNs. - Integrated ISBN normalization in the list command for readable formatting. - Added comprehensive unit tests and doctests for the new helper. --- CHANGELOG.md | 26 +++++++++-- Cargo.lock | 74 +++++++++++++++++++++++++++---- Cargo.toml | 1 + src/commands/add_book.rs | 25 ++++++++--- src/commands/list.rs | 22 +++++++--- src/i18n/locales/en.json | 12 ++++- src/i18n/locales/it.json | 12 ++++- src/models/book.rs | 18 ++++---- src/utils/isbn.rs | 94 ++++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + 10 files changed, 252 insertions(+), 33 deletions(-) create mode 100644 src/utils/isbn.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d848b62..fc8387b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,22 @@ All notable changes to this project will be documented in this file. -## [0.4.0] - 2025-10-18 +## [0.4.0] - 2025-10-18 (in progress) ### Added - New command `add book`: - - Fetches book information automatically from the Google Books API using ISBN. - - Populates title, author, editor, year, language, genre, and summary automatically. - - Fallback to interactive mode (planned) for books not found. + - Fetches book information automatically from the Google Books API using ISBN. + - Populates title, author, editor, year, language, genre, and summary automatically. + - Fallback to interactive mode (planned) for books not found. - Integrated dynamic i18n support for all CLI help messages (`add`, `book`, `isbn`). - Added automatic language name resolution (e.g., `"it"` β†’ `"Italian"`). - New utility module `utils/lang.rs` for ISO 639-1 to language name conversion. +- **New utility module `utils/isbn.rs`:** + - Introduced the `normalize_isbn()` helper for validation and bidirectional formatting. + - Supports both ISBN-10 and ISBN-13 with hyphenation handling. + - Returns localized error messages for invalid, undefined, or malformed ISBNs. + - Includes comprehensive unit tests and doctests. - Localized console messages for book lookup and insertion results. ### Changed @@ -20,11 +25,13 @@ All notable changes to this project will be documented in this file. - Modularized command structure: added `add.rs` and `add_book.rs` under `src/commands/`. - Improved error handling for Google Books API responses and JSON decoding. - Replaced manual `impl Default` blocks with idiomatic `#[derive(Default)]`. +- Enhanced ISBN display formatting in the `list` command using `normalize_isbn()` for readable hyphenated output. ### Fixed - Deserialization issues with Google Books API fields (`volumeInfo`, `publishedDate`, `pageCount`). - Empty fields on insertion caused by incorrect field mapping. +- Prevented duplicate ISBN insertion with user-friendly message (`"Book already present in your library"`). ### Example usage @@ -34,6 +41,17 @@ $ librius add book --isbn 9788820382698 πŸ“˜ Libro trovato: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) βœ… Libro β€œLa lingua dell'antico Egitto” aggiunto con successo. +$ librius list --short + +πŸ“š Your Library + +β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ID β”‚ Title β”‚ Author β”‚ Editor β”‚ Year β”‚ ISBN β”‚ +β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 91 β”‚ The Hobbit β”‚ J.R.R. Tolkien β”‚ Allen & Unwin β”‚ 1937 β”‚ 978-0-345-33968-3 β”‚ +β”‚ 92 β”‚ Foundation β”‚ Isaac Asimov β”‚ Gnome Press β”‚ 1951 β”‚ 978-0-553-80371-0 β”‚ +| 128 β”‚ La lingua dell'antico Egitto β”‚ Emanuele M. Ciampini β”‚ Lingue antiche del Vicino Oriente e del Mediterraneo β”‚ 2018 β”‚ 978-88-203-8269-8 β”‚ +β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- diff --git a/Cargo.lock b/Cargo.lock index 2ba2440..3e19e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -301,6 +307,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "codegen" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34c59e8a9b988977ec3bd61ab380cf1167048817ecd3d6999fac03657f85a609" +dependencies = [ + "indexmap 1.9.3", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -463,7 +478,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -505,7 +520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -697,13 +712,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1015,6 +1036,16 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -1057,6 +1088,17 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "isbn2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ec02acd9e12bee98c320d8ff9fe1c8882174067ac7932866f7ad7bf5309cd8" +dependencies = [ + "arrayvec", + "codegen", + "roxmltree", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1122,6 +1164,7 @@ dependencies = [ "csv", "dirs", "flate2", + "isbn2", "once_cell", "reqwest", "rusqlite", @@ -1589,6 +1632,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671" +dependencies = [ + "xmlparser", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -1613,7 +1665,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1754,7 +1806,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -1932,7 +1984,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2640,6 +2692,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.8.0" @@ -2769,7 +2827,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap", + "indexmap 2.11.4", "memchr", "thiserror 2.0.17", "zopfli", @@ -2790,7 +2848,7 @@ dependencies = [ "flate2", "getrandom 0.3.4", "hmac", - "indexmap", + "indexmap 2.11.4", "lzma-rust2", "memchr", "pbkdf2", diff --git a/Cargo.toml b/Cargo.toml index dfa6151..8c3c59c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ umya-spreadsheet = "2.3.3" csv = "1.3.1" tabled = "0.20.0" reqwest = { version = "0.12.24", features = ["blocking", "json"] } +isbn2 = "0.4.0" [target.'cfg(windows)'.dependencies] zip = "6.0.0" diff --git a/src/commands/add_book.rs b/src/commands/add_book.rs index ee2948f..be46a6d 100644 --- a/src/commands/add_book.rs +++ b/src/commands/add_book.rs @@ -1,10 +1,11 @@ use crate::i18n::tr; +use crate::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 chrono::Utc; use reqwest::blocking::get; -use rusqlite::Connection; +use rusqlite::{Connection, Error as RusqliteError, ErrorCode}; use serde::Deserialize; use std::error::Error; @@ -78,7 +79,7 @@ pub fn handle_add_book(conn: &Connection, isbn: &str) -> Result<(), Box().ok()) .unwrap_or_default(), - isbn: isbn.to_string(), + isbn: normalize_isbn(isbn, true).unwrap_or_default(), language: info .language .as_ref() @@ -93,7 +94,7 @@ pub fn handle_add_book(conn: &Connection, isbn: &str) -> Result<(), Box Result<(), Box { + print_ok(&tr_with("add.success", &[("title", &new_book.title)]), true); - print_ok(&tr_with("add.success", &[("title", &new_book.title)]), true); + }, + Err(e) => { + if let RusqliteError::SqliteFailure(err, _) = &e { + if err.code == ErrorCode::ConstraintViolation { + print_warn(&tr("add.duplicate_isbn")); + } else { + print_err(&tr("add.sql_error")); + } + } else { + print_err(&e.to_string()); + } + } + } } else { print_warn(&tr("add.no_result")); } diff --git a/src/commands/list.rs b/src/commands/list.rs index ad0536f..d61de6c 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,5 +1,6 @@ use crate::book::{Book, 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; @@ -24,13 +25,25 @@ 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) { + Ok(formatted) => formatted, + Err(e) => { + print_err(&e.to_string()); + isbn_plain.clone() + } // fallback in caso di ISBN non valido + }; + Ok(Book { id: row.get("id")?, title: row.get("title")?, author: row.get("author")?, editor: row.get("editor")?, year: row.get("year")?, - isbn: row.get("isbn")?, + isbn: isbn_formatted, language: row.get("language")?, pages: row.get("pages")?, genre: row.get("genre")?, @@ -73,16 +86,13 @@ pub fn handle_list( let mut books: Vec = Vec::new(); // Build owned params: store boxed ToSql trait objects so ownership is - // guaranteed and we can build a slice of `&dyn ToSql` for the query. + // guaranteed, and we can build a slice of `&dyn ToSql` for the query. let mut params_owned: Vec> = Vec::new(); if let Some(v) = id { params_owned.push(Box::new(v)); } // Create a slice of references to pass to rusqlite - let params_refs: Vec<&dyn ToSql> = params_owned - .iter() - .map(|b| b.as_ref() as &dyn ToSql) - .collect(); + let params_refs: Vec<&dyn ToSql> = params_owned.iter().map(|b| b.as_ref()).collect(); let mapped = stmt.query_map(params_refs.as_slice(), row_to_book)?; for r in mapped { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8a9414a..9668b77 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -93,6 +93,7 @@ "list.header.author": "Author", "list.header.editor": "Editor", "list.header.year": "Year", + "list.header.ISBN": "ISBN", "list.header.language": "Language", "list.header.room": "Room", "list.header.shelf": "Shelf", @@ -123,5 +124,14 @@ "help.unknown_command": "Unknown command. Use --help to see available options.", "book.add.http_error": "HTTP error while fetching book data: {error}", "book.add.book_info": "Parsed book info:\n{info}\n", - "add.decode_error": "Error decoding book data: {error}" + "add.decode_error": "Error decoding book data: {error}", + "add.duplicate_isbn": "Book already present in your library.", + "add.sql_error": "Database error while saving the book.", + "book.isbn.invalid_checksum": "Invalid ISBN checksum: {isbn}.", + "book.isbn.invalid_length": "Invalid ISBN length: {isbn}.", + "book.isbn.invalid_conversion": "Invalid ISBN conversion: {isbn}.", + "book.isbn.invalid_digit": "Invalid digit in ISBN number: {isbn}.", + "book.isbn.digit_too_large": "Digit too large in ISBN number: {isbn}.", + "book.isbn.invalid_group": "Invalid group identifier in ISBN: {isbn}.", + "book.isbn.undefined_range": "Undefined range in ISBN: {isbn}." } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index a41ecc7..c679729 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -93,6 +93,7 @@ "list.header.author": "Autore", "list.header.editor": "Editore", "list.header.year": "Anno", + "list.header.ISBN": "ISBN", "list.header.language": "Lingua", "list.header.room": "Stanza", "list.header.shelf": "Scaffale", @@ -123,5 +124,14 @@ "help.unknown_command": "Comando sconosciuto. Usa --help per vedere le opzioni disponibili.", "book.add.http_error": "Errore HTTP durante il recupero dei dati del libro: {error}", "book.add.book_info": "Informazioni sul libro analizzate:\n{info}\n", - "add.decode_error": "Errore durante la decodifica dei dati del libro: {error}" + "add.decode_error": "Errore durante la decodifica dei dati del libro: {error}", + "add.duplicate_isbn": "Libro giΓ  presente in biblioteca.", + "add.sql_error": "Errore del database durante il salvataggio del libro.", + "book.isbn.invalid_checksum": "Checksum ISBN non valido: {isbn}.", + "book.isbn.invalid_length": "Lunghezza ISBN non valida: {isbn}.", + "book.isbn.invalid_conversion": "Conversione ISBN non valida: {isbn}.", + "book.isbn.invalid_digit": "Cifra non valida nel numero ISBN: {isbn}.", + "book.isbn.digit_too_large": "Cifra troppo grande nel numero ISBN: {isbn}.", + "book.isbn.invalid_group": "Identificatore di gruppo non valido nell'ISBN: {isbn}.", + "book.isbn.undefined_range": "Intervallo indefinito nell'ISBN: {isbn}." } diff --git a/src/models/book.rs b/src/models/book.rs index 28e0278..cccb0db 100644 --- a/src/models/book.rs +++ b/src/models/book.rs @@ -31,11 +31,11 @@ impl<'a> Tabled for BookFull<'a> { 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()); + /*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()), @@ -43,11 +43,11 @@ impl<'a> Tabled for BookFull<'a> { 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("-")), - Cow::from(added_date), ] } @@ -58,17 +58,17 @@ impl<'a> Tabled for BookFull<'a> { 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")), - Cow::from(tr("list.header.added")), ] } } impl<'a> Tabled for BookShort<'a> { - const LENGTH: usize = 5; + const LENGTH: usize = 6; fn fields(&self) -> Vec> { let b = self.0; @@ -78,6 +78,7 @@ impl<'a> Tabled for BookShort<'a> { Cow::from(&b.author), Cow::from(&b.editor), Cow::from(b.year.to_string()), + Cow::from(b.isbn.to_string()), ] } @@ -88,6 +89,7 @@ impl<'a> Tabled for BookShort<'a> { 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/utils/isbn.rs b/src/utils/isbn.rs new file mode 100644 index 0000000..14ceec0 --- /dev/null +++ b/src/utils/isbn.rs @@ -0,0 +1,94 @@ +use crate::tr_with; +use isbn2::{Isbn, IsbnError}; +use std::str::FromStr; + +/// Normalize and validate an ISBN string. +/// +/// - If `is_plain == true`, returns the ISBN without hyphens. +/// - If `is_plain == false`, returns the ISBN with hyphens. +/// - Works with both ISBN-10 and ISBN-13. +/// - Returns an error string if validation fails. +/// +/// # Examples +/// ``` +/// # use librius::isbn::normalize_isbn; +/// +/// let plain = normalize_isbn("978-88-203-8269-8", true).unwrap(); +/// assert_eq!(plain, "9788820382698"); +/// +/// let pretty = normalize_isbn("9788820382698", false).unwrap(); +/// assert_eq!(pretty, "978-88-203-8269-8"); +/// ``` +pub fn normalize_isbn(isbn_input: &str, is_plain: bool) -> Result { + let cleaned = isbn_input.trim().replace([' ', '-'], ""); + + match Isbn::from_str(&cleaned) { + Ok(isbn) => { + let plain = isbn.to_string(); + let formatted = isbn + .hyphenate() + .unwrap_or_else(|_| plain.clone().parse().unwrap()); + + Ok(if is_plain { + plain + } else { + formatted.parse().unwrap() + }) + } + Err(IsbnError::InvalidChecksum) => Err(format!( + "\n{}", + &tr_with("book.isbn.invalid_checksum", &[("isbn", isbn_input)]), + )), + Err(IsbnError::InvalidLength) => Err(format!( + "\n{}", + &tr_with("book.isbn.invalid_length", &[("isbn", isbn_input)]) + )), + Err(IsbnError::InvalidConversion) => Err(format!( + "\n{}", + &tr_with("book.isbn.invalid_conversion", &[("isbn", isbn_input)]) + )), + Err(IsbnError::InvalidDigit) => Err(format!( + "\n{}", + &tr_with("book.isbn.invalid_digit", &[("isbn", isbn_input)]) + )), + Err(IsbnError::DigitTooLarge) => Err(format!( + "\n{}", + &tr_with("book.isbn.digit_too_large", &[("isbn", isbn_input)]) + )), + Err(IsbnError::InvalidGroup) => Err(format!( + "\n{}", + &tr_with("book.isbn.invalid_group", &[("isbn", isbn_input)]) + )), + Err(IsbnError::UndefinedRange) => Err(format!( + "\n{}", + &tr_with("book.isbn.undefined_range", &[("isbn", isbn_input)]) + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_plain_output() { + let r = normalize_isbn("978-88-203-8269-8", true).unwrap(); + assert_eq!(r, "9788820382698"); + } + + #[test] + fn test_hyphenated_output() { + let r = normalize_isbn("9788820382698", false).unwrap(); + assert_eq!(r, "978-88-203-8269-8"); + } + + #[test] + fn test_invalid_length() { + assert!(normalize_isbn("12345", true).is_err()); + } + + #[test] + fn test_invalid_characters() { + assert!(normalize_isbn("97A88203826B8", false).is_err()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 62bd63e..174cf07 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ // Contiene funzioni di supporto generali e costanti // grafiche per output CLI. // ===================================================== +pub mod isbn; pub mod lang; pub mod table; From d04944003afbfeae5669fbb377bcef1d18cc321a Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 20 Oct 2025 16:02:28 +0200 Subject: [PATCH 3/4] release: bump to v0.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Finalized version 0.4.0 with new `edit book` command. - Added dynamic field generation via `EDITABLE_FIELDS` and automatic language conversion. - Introduced ordered and grouped CLI help using `display_order()` and `next_help_heading()`. - Added localized detailed field update messages showing old β†’ new values. - Implemented plural-aware summary messages for all languages (e.g., "campo"/"campi"). - Refactored CLI structure for consistency, readability, and full i18n support. --- CHANGELOG.md | 68 ++++++++-- README.md | 95 +++++++++----- src/cli.rs | 269 +++++++++++++++++++++++--------------- src/commands/edit_book.rs | 125 ++++++++++++++++++ src/commands/mod.rs | 2 + src/db/books.rs | 94 +++++++++++++ src/db/mod.rs | 2 + src/i18n/locales/en.json | 29 +++- src/i18n/locales/it.json | 29 +++- src/utils/fields.rs | 14 ++ src/utils/mod.rs | 1 + 11 files changed, 579 insertions(+), 149 deletions(-) create mode 100644 src/commands/edit_book.rs create mode 100644 src/db/books.rs create mode 100644 src/utils/fields.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8387b..075c83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [0.4.0] - 2025-10-18 (in progress) +## [0.4.0] - 2025-10-18 ### Added @@ -10,7 +10,24 @@ All notable changes to this project will be documented in this file. - Fetches book information automatically from the Google Books API using ISBN. - Populates title, author, editor, year, language, genre, and summary automatically. - Fallback to interactive mode (planned) for books not found. -- Integrated dynamic i18n support for all CLI help messages (`add`, `book`, `isbn`). +- New command `edit book`: + - Allows updating any existing book record by ID or ISBN. + - Supports all editable fields (`title`, `author`, `editor`, `year`, `language`, `pages`, + `genre`, `summary`, `room`, `shelf`, `row`, `position`), excluding ID and ISBN. + - Automatically converts language codes (e.g., `"en" β†’ "English"`) using `lang_code_to_name()`. + - Dynamically generates CLI arguments for each editable field via a centralized + `EDITABLE_FIELDS` definition in `fields.rs`. + - Grouped and ordered help output using `display_order()` and `next_help_heading()`: + - Global options appear first. + - Book-specific options are clearly grouped under titled sections. + - Field updates now display **localized detailed messages**: + - e.g. `βœ… Field "year" updated successfully (2018 β†’ 2020).` + - Shows both the previous and new values for each modified field. + - Final update summary message supports **language-aware pluralization**: + - English: `"βœ… Book 9788820382698 successfully updated (2 fields modified)."` + - Italian: `"βœ… Libro 9788820382698 aggiornato correttamente (2 campi modificati)."` + +- Integrated dynamic i18n support for all CLI help messages (`add`, `edit`, `book`, `isbn`). - Added automatic language name resolution (e.g., `"it"` β†’ `"Italian"`). - New utility module `utils/lang.rs` for ISO 639-1 to language name conversion. - **New utility module `utils/isbn.rs`:** @@ -18,14 +35,18 @@ All notable changes to this project will be documented in this file. - Supports both ISBN-10 and ISBN-13 with hyphenation handling. - Returns localized error messages for invalid, undefined, or malformed ISBNs. - Includes comprehensive unit tests and doctests. -- Localized console messages for book lookup and insertion results. +- Localized console messages for book lookup, edition, and insertion results. ### Changed -- Modularized command structure: added `add.rs` and `add_book.rs` under `src/commands/`. +- Modularized command structure: added `add.rs`, `add_book.rs`, and `edit_book.rs` under `src/commands/`. +- Unified language handling logic between `add` and `edit` commands. - Improved error handling for Google Books API responses and JSON decoding. - Replaced manual `impl Default` blocks with idiomatic `#[derive(Default)]`. - Enhanced ISBN display formatting in the `list` command using `normalize_isbn()` for readable hyphenated output. +- Refactored CLI (`cli.rs`) with ordered, grouped, and localized help output for all commands. +- Localized final book update message with plural-sensitive translation keys: + - `"edit.book.updated.one"` and `"edit.book.updated.many"` in `en.json` / `it.json`. ### Fixed @@ -36,11 +57,13 @@ All notable changes to this project will be documented in this file. ### Example usage ```bash +# βž• Add a new book automatically using its ISBN $ librius add book --isbn 9788820382698 -πŸ” Ricerca del libro con ISBN: 9788820382698 -πŸ“˜ Libro trovato: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) -βœ… Libro β€œLa lingua dell'antico Egitto” aggiunto con successo. +πŸ” Searching for book with ISBN: 9788820382698 +πŸ“˜ Found: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) +βœ… Book β€œLa lingua dell'antico Egitto” successfully added to your library. +# πŸ“š List all books (compact view) $ librius list --short πŸ“š Your Library @@ -50,8 +73,37 @@ $ librius list --short β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 91 β”‚ The Hobbit β”‚ J.R.R. Tolkien β”‚ Allen & Unwin β”‚ 1937 β”‚ 978-0-345-33968-3 β”‚ β”‚ 92 β”‚ Foundation β”‚ Isaac Asimov β”‚ Gnome Press β”‚ 1951 β”‚ 978-0-553-80371-0 β”‚ -| 128 β”‚ La lingua dell'antico Egitto β”‚ Emanuele M. Ciampini β”‚ Lingue antiche del Vicino Oriente e del Mediterraneo β”‚ 2018 β”‚ 978-88-203-8269-8 β”‚ +β”‚128 β”‚ La lingua dell'antico Egitto β”‚ Emanuele M. Ciampini β”‚ Lingue antiche del Vicino Oriente e del Mediterraneo β”‚ 2018 β”‚ 978-88-203-8269-8 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +# ✏️ Edit an existing record (by ISBN or ID) +$ librius edit book 9788820382698 --year 2020 +πŸ“ Updating book with ISBN 9788820382698... +βœ… Field β€œyear” updated successfully (2018 β†’ 2020) + +# 🌍 Update language using ISO code (automatically converted) +$ librius edit book 9788820382698 --lang_book en +πŸ“ Updating book language... +βœ… Field β€œlanguage” updated successfully (β€œItalian” β†’ β€œEnglish”) + +# πŸ“– Display detailed information +$ librius list --id 128 --details + +πŸ“˜ Book Details (ID 128) +──────────────────────────────────────────────────────────────────────────── +Title: La lingua dell'antico Egitto +Author: Emanuele M. Ciampini +Editor: Lingue antiche del Vicino Oriente e del Mediterraneo +Year: 2020 +Language: English +Genre: Linguistics +Pages: 432 +Room: B +Shelf: 4 +Row: 2 +Position: 5 +ISBN: 978-88-203-8269-8 +──────────────────────────────────────────────────────────────────────────── ``` --- diff --git a/README.md b/README.md index c0f652d..7f41568 100644 --- a/README.md +++ b/README.md @@ -25,28 +25,35 @@ and import/export support. ## ✨ New in v0.4.0 -**πŸ“š Automatic Book Fetching via Google Books API** - -- Introduced the new command `librius add book --isbn `. -- Automatically retrieves metadata from the Google Books API: - - Title, Author(s), Publisher, Year, Language, Category, Summary. -- The command inserts the record directly into your local database. -- If the book is not found, Librius will later support interactive entry mode. - -**🌍 Localized Help and Language Mapping** - -- All help messages are dynamically localized (English / Italian). -- Language codes like `"it"` are automatically expanded to `"Italian"`. -- Improved JSON decoding and error reporting for external API calls. - -Example: - -```bash -$ librius add book --isbn 9788820382698 -πŸ” Searching for book with ISBN: 9788820382698 -πŸ“˜ Book found: β€œLa lingua dell'antico Egitto” β€” Emanuele M. Ciampini (2018) -βœ… Book β€œLa lingua dell'antico Egitto” added successfully. -``` +**πŸ†• Edit command** + +- New command `edit book`: + - Allows editing existing records by **ID** or **ISBN**. + - Supports all editable fields: + `title`, `author`, `editor`, `year`, `language`, `pages`, + `genre`, `summary`, `room`, `shelf`, `row`, `position`. + - Automatically converts language codes (e.g., `en β†’ English`) via `lang_code_to_name()`. + - Dynamically generates CLI arguments from a single `EDITABLE_FIELDS` list. + - Field updates now show **old and new values**: + ``` + βœ… Field β€œyear” updated successfully (2018 β†’ 2020). + βœ… Field β€œlanguage” updated successfully (Italian β†’ English). + ``` + - Final summary messages are **plural-aware and localized**: + - English β†’ `βœ… Book 9788820382698 successfully updated (2 fields modified).` + - Italian β†’ `βœ… Libro 9788820382698 aggiornato correttamente (2 campi modificati).` + +**🌍 Internationalized CLI** + +- All commands and help messages are fully localized (`en` / `it`). +- Ordered and grouped help output using `display_order()` and `next_help_heading()`. +- Dynamic translations for fields, subcommands, and error messages. + +**πŸ“— Improved structure** + +- Modular command layout (`add`, `edit`, `list`, `import`, `export`, `backup`, `config`). +- Centralized field definitions in `fields.rs` for consistent behavior. +- Cleaner `cli.rs` with display order and grouped help sections. --- @@ -83,18 +90,19 @@ cargo install rtimelogger ## βš™οΈ Features -| Status | Feature | Description | -|:------:|:-------------------------|:----------------------------------------------------------------------------| -| βœ… | **List** | Display all books stored in the local database | -| βœ… | **Config management** | Manage YAML config via `config --print`, `--init`, `--edit`, and `--editor` | -| βœ… | **Backup** | Create plain or compressed database backups (`.sqlite`, `.zip`, `.tar.gz`) | -| βœ… | **Export** | Export data in CSV, JSON, or XLSX format | -| βœ… | **Import** | Import data from CSV or JSON files (duplicate-safe via ISBN) | -| βœ… | **Database migrations** | Automatic schema upgrades at startup | -| βœ… | **Logging system** | Records operations and migrations in log table | -| βœ… | **Multilanguage (i18n)** | Localized CLI (commands, help); embedded JSON; `--lang` + YAML `language` | -| 🚧 | **Add / Remove** | Add or delete books via CLI commands | -| 🚧 | **Search** | Search by title, author, or ISBN | +| Feature | Command | Description | +|:-------------------------|:---------------------------------|:--------------------------------------------------------------------------------------------------------------| +| **List** | `librius list` | Display all books stored in the local database, in full or compact view | +| **Config management** | `librius config` | Manage YAML configuration via `--print`, `--init`, `--edit`, `--editor` | +| **Backup** | `librius backup` | Create plain or compressed database backups (`.sqlite`, `.zip`, `.tar.gz`) | +| **Export** | `librius export` | Export data in CSV, JSON, or XLSX format | +| **Import** | `librius import` | Import data from CSV or JSON files (duplicate-safe via ISBN) | +| **Database migrations** | *(automatic)* | Automatic schema upgrades and integrity checks at startup | +| **Logging system** | *(internal)* | Records all operations and migrations in an internal log table | +| **Multilanguage (i18n)** | `librius --lang ` | Fully localized CLI (commands, help, messages); `--lang` flag and config key | +| **Add book** | `librius add book --isbn ` | Add new books using ISBN lookup via Google Books API | +| **Edit book** | `librius edit book ` | Edit existing records by ID or ISBN; dynamic field generation, language conversion, and plural-aware messages | +| **Dynamic help system** | `librius help ` | Ordered and grouped help output using `display_order()` and `next_help_heading()` | --- @@ -121,6 +129,25 @@ $ librius list --short β”‚ 1 β”‚ The Rust Programming Languageβ”‚ Steve Klabnik β”‚ No Starch β”‚ 2018 β”‚ β”‚ 2 β”‚ Clean Code β”‚ Robert C. Martin β”‚ Pearson β”‚ 2008 β”‚ β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜ + + +# βž• Add a book automatically by ISBN +$ librius add book --isbn 9788820382698 +πŸ“˜ Book β€œLa lingua dell'antico Egitto” successfully added. + +# ✏️ Edit book details +$ librius edit book 9788820382698 --year 2020 +βœ… Field β€œyear” updated successfully (2018 β†’ 2020). +βœ… Book 9788820382698 successfully updated (1 field modified). + +# 🌍 Update language (auto conversion from ISO) +$ librius edit book 9788820382698 --lang_book en +βœ… Field β€œlanguage” updated successfully (Italian β†’ English). + +# πŸ“š List your library (compact view) +$ librius list --short + + ``` --- diff --git a/src/cli.rs b/src/cli.rs index 91897b5..513a31c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ -use crate::commands::{handle_config, handle_list}; +use crate::commands::{handle_config, handle_edit_book, handle_list}; +use crate::fields::EDITABLE_FIELDS; use crate::i18n::{tr, tr_s}; use crate::tr_with; use crate::utils::print_err; -use clap::{Arg, Command, Subcommand}; +use clap::{Arg, ArgAction, Command, Subcommand}; use rusqlite::Connection; /// Costruisce la CLI localizzata usando le stringhe giΓ  caricate in memoria. pub fn build_cli() -> Command { - // Disabilitiamo help/subcommand automatici per poter localizzare/spiegare noi Command::new(tr_s("app_name")) .version(env!("CARGO_PKG_VERSION")) .about(tr_s("app_about")) @@ -18,110 +18,147 @@ pub fn build_cli() -> Command { .short('h') .long("help") .help(tr_s("help_flag_about")) - .action(clap::ArgAction::Help) - .global(true), + .action(ArgAction::Help) + .global(true) + .help_heading("Global options") + .display_order(1), ) .arg( Arg::new("verbose") .short('v') .long("verbose") - .global(true) .help(tr_s("help_verbose")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .global(true) + .help_heading("Global options") + .display_order(2), ) .arg( Arg::new("lang") .short('l') .long("lang") + .help(tr_s("help_lang")) .global(true) .num_args(1) - .help(tr_s("help_lang")), + .help_heading("Global options") + .display_order(3), ) + // πŸ“˜ list command .subcommand( Command::new("list") .about(tr_s("list_about")) + .display_order(10) .arg( Arg::new("short") .long("short") .help(tr_s("help.list.short")) - .action(clap::ArgAction::SetTrue) - .num_args(0), + .action(ArgAction::SetTrue) + .help_heading("List-specific options") + .display_order(11), ) .arg( Arg::new("id") .long("id") - .help(tr_s("help.list.id")) // es: "Specify the record ID to show" + .help(tr_s("help.list.id")) .value_name("ID") .num_args(1) - .value_parser(clap::value_parser!(i32)), + .value_parser(clap::value_parser!(i32)) + .help_heading("List-specific options") + .display_order(12), ) .arg( Arg::new("details") .long("details") - .help(tr_s("help.list.details")) // es: "Show all fields of the specified record (requires --id)" - .action(clap::ArgAction::SetTrue) - .num_args(0), + .help(tr_s("help.list.details")) + .action(ArgAction::SetTrue) + .help_heading("List-specific options") + .display_order(13), ), ) + // βš™οΈ config command .subcommand( Command::new("config") .about(tr_s("config_about")) + .display_order(20) .arg( Arg::new("init") .long("init") .help(tr_s("config_init_help")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Config-specific options") + .display_order(21), ) .arg( Arg::new("print") .long("print") .help(tr_s("config_print_help")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Config-specific options") + .display_order(22), ) .arg( Arg::new("edit") .long("edit") .help(tr_s("config_edit_help")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Config-specific options") + .display_order(23), ) .arg( Arg::new("editor") .long("editor") .requires("edit") .num_args(1) - .help(tr_s("config_editor_help")), + .help(tr_s("config_editor_help")) + .help_heading("Config-specific options") + .display_order(24), ), ) + // πŸ’Ύ backup command .subcommand( - Command::new("backup").about(tr_s("backup_about")).arg( - Arg::new("compress") - .long("compress") - .help(tr_s("backup_compress_help")) - .action(clap::ArgAction::SetTrue), - ), + Command::new("backup") + .about(tr_s("backup_about")) + .display_order(30) + .arg( + Arg::new("compress") + .long("compress") + .help(tr_s("backup_compress_help")) + .action(ArgAction::SetTrue) + .help_heading("Backup-specific options") + .display_order(31), + ), ) + // πŸ“€ export command .subcommand( Command::new("export") .about(tr_s("export_about")) + .display_order(40) .arg( Arg::new("csv") .long("csv") .help(tr_s("export_csv_help")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with_all(["xlsx", "json"]) + .help_heading("Export-specific options") + .display_order(41), ) .arg( Arg::new("xlsx") .long("xlsx") .help(tr_s("export_xlsx_help")) .conflicts_with_all(["csv", "json"]) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Export-specific options") + .display_order(42), ) .arg( Arg::new("json") .long("json") .help(tr_s("export_json_help")) .conflicts_with_all(["csv", "xlsx"]) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Export-specific options") + .display_order(43), ) .arg( Arg::new("output") @@ -129,32 +166,43 @@ pub fn build_cli() -> Command { .long("output") .help(tr_s("export_output_help")) .value_name("FILE") - .required(false), + .required(false) + .help_heading("Export-specific options") + .display_order(44), ), ) + // πŸ“₯ import command .subcommand( Command::new("import") .about(tr_s("import_about")) + .display_order(50) .arg( Arg::new("file") .short('f') .long("file") .help(tr_s("import_file_help")) .required(true) - .value_name("PATH"), + .value_name("PATH") + .help_heading("Import-specific options") + .display_order(51), ) .arg( Arg::new("csv") .long("csv") .help(tr_s("import_csv_help")) - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .conflicts_with("json") + .help_heading("Import-specific options") + .display_order(52), ) .arg( Arg::new("json") .long("json") .help(tr_s("import_json_help")) .conflicts_with("csv") - .action(clap::ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .help_heading("Import-specific options") + .display_order(53), ) .arg( Arg::new("delimiter") @@ -164,37 +212,86 @@ pub fn build_cli() -> Command { .num_args(1) .value_name("CHAR") .required(false) - .value_parser(clap::builder::NonEmptyStringValueParser::new()), + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .help_heading("Import-specific options") + .display_order(54), ), ) + // βž• add book command .subcommand( - Command::new("add").about(tr("help.add.about")).subcommand( - Command::new("book").about(tr("help.add.book.about")).arg( - Arg::new("isbn") - .long("isbn") - .help(tr("help.add.book.isbn")) - .required(true) - .value_name("ISBN"), + Command::new("add") + .about(tr("help.add.about")) + .display_order(60) + .subcommand( + Command::new("book") + .about(tr("help.add.book.about")) + .display_order(61) + .arg( + Arg::new("isbn") + .long("isbn") + .help(tr_s("help.add.book.isbn")) + .required(true) + .value_name("ISBN") + .help_heading("Add Book specific options") + .display_order(62), + ), ), - ), ) - // help come subcommand dedicato (es: `librius help config`) + // ✏️ edit book command (aggiornato con logica ibrida ID/ISBN) .subcommand( - Command::new("help").about(tr_s("help_flag_about")).arg( - Arg::new("command") - .value_name("COMMAND") - .help(tr_s("help_lang")) // riuso chiave esistente per avere testo localizzato - .num_args(1), - ), + Command::new("edit") + .about(tr("help.edit.about")) + .display_order(70) + .subcommand({ + let mut cmd = Command::new("book") + .about(tr("help.edit.book.about")) + .display_order(71) + .arg( + Arg::new("key") + .help(tr_s("help.edit.book.key")) + .required(true) + .num_args(1) + .help_heading("Edit Book required option") + .display_order(72), + ); + + // βœ… Aggiunta dinamica di tutti i campi editabili + for (i, (name, help, short)) in EDITABLE_FIELDS.iter().enumerate() { + cmd = cmd.arg( + Arg::new(*name) + .long(*name) + .short(*short) + .help(tr_s(*help)) + .num_args(1) + .action(ArgAction::Set) + .help_heading("Edit Book specific options") + .display_order(80 + i), + ); + } + + cmd + }), + ) + .subcommand( + Command::new("help") + .about(tr_s("help_flag_about")) + .display_order(200) + .arg( + Arg::new("command") + .value_name("COMMAND") + .help(tr_s("help_lang")) + .num_args(1) + .display_order(201), + ), ) } -/// Esegue il parsing della CLI localizzata +/// Parsing CLI pub fn parse_cli() -> clap::ArgMatches { build_cli().get_matches() } -/// Esegue il comando selezionato +/// Dispatch principale dei comandi pub fn run_cli( matches: &clap::ArgMatches, conn: &mut Connection, @@ -218,14 +315,17 @@ pub fn run_cli( editor, }; Ok(handle_config(&cmd)?) + } else if let Some(("edit", sub_m)) = matches.subcommand() { + if let Some(("book", book_m)) = sub_m.subcommand() { + handle_edit_book(conn, book_m)?; // βœ… integrazione comando edit book + } + Ok(()) } else if let Some(("backup", sub_m)) = matches.subcommand() { let compress = sub_m.get_flag("compress"); - // esegue backup plain o compresso (zip su Windows, tar.gz su Unix) crate::commands::handle_backup(conn, compress)?; Ok(()) } else if let Some(("export", sub_m)) = matches.subcommand() { let output_path = sub_m.get_one::("output").cloned(); - let export_csv = sub_m.get_flag("csv"); let export_xlsx = sub_m.get_flag("xlsx"); let export_json = sub_m.get_flag("json"); @@ -239,7 +339,6 @@ pub fn run_cli( } Ok(()) } else if let Some(("import", sub_m)) = matches.subcommand() { - // πŸ”Ή Recupera il percorso del file da importare let file_path = sub_m.get_one::("file").cloned(); if file_path.is_none() { print_err(&tr("import.error.missing_file")); @@ -247,19 +346,12 @@ pub fn run_cli( } let file = file_path.unwrap(); - - // πŸ”Ή Determina il formato (default CSV) - let _import_csv = sub_m.get_flag("csv"); let import_json = sub_m.get_flag("json"); + let delimiter_char = sub_m + .get_one::("delimiter") + .and_then(|s| s.chars().next()) + .unwrap_or(','); - // πŸ”Ή Recupera delimitatore opzionale (solo CSV) - let delimiter_char = if let Some(delim_str) = sub_m.get_one::("delimiter") { - delim_str.chars().next().unwrap_or(',') - } else { - ',' - }; - - // πŸ”Ή Esegui l’import nel formato corretto let result = if import_json { crate::commands::handle_import_json(conn, &file) } else { @@ -284,18 +376,19 @@ pub fn run_cli( } Ok(()) } else if let Some(("help", sub_m)) = matches.subcommand() { - if let Some(cmd_name) = sub_m.get_one::("command") { - // Stampa help del sotto-comando se esiste - if let Some(sc) = build_cli().find_subcommand(cmd_name) { - sc.clone().print_help()?; // clone perchΓ© Command non implementa Copy - println!(); - return Ok(()); - } + if let Some(cmd_name) = sub_m.get_one::("command") + && let Some(sc) = build_cli().find_subcommand(cmd_name) + { + sc.clone().print_help()?; + println!(); + return Ok(()); } build_cli().print_help()?; println!(); Ok(()) } else { + build_cli().print_help()?; + println!(); Ok(()) } } @@ -312,37 +405,3 @@ pub enum Commands { }, Help, } - -#[cfg(test)] -mod tests_cli { - use super::*; - use crate::i18n::load_language; - - #[test] - fn config_help_flags_no_value_placeholders() { - load_language("en"); - let mut cmd = build_cli(); - // Trova subcommand config - let sc = cmd - .find_subcommand_mut("config") - .expect("subcommand config esiste"); - let mut help_buf: Vec = Vec::new(); - sc.write_help(&mut help_buf).expect("help scritto"); - let help = String::from_utf8(help_buf).unwrap(); - assert!( - !help.contains(""), - "--init non deve richiedere valore: {}", - help - ); - assert!( - !help.contains(""), - "--print non deve richiedere valore: {}", - help - ); - assert!( - !help.contains(""), - "--edit non deve richiedere valore: {}", - help - ); - } -} diff --git a/src/commands/edit_book.rs b/src/commands/edit_book.rs new file mode 100644 index 0000000..fe34230 --- /dev/null +++ b/src/commands/edit_book.rs @@ -0,0 +1,125 @@ +use crate::db::books::{get_book_fields, update_book_by_id, update_book_by_isbn}; +use crate::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; + +pub fn handle_edit_book(conn: &Connection, matches: &clap::ArgMatches) -> rusqlite::Result<()> { + let key = matches + .get_one::("key") + .expect("Book ID or ISBN is required"); + + let mut fields = HashMap::new(); + + println!(); + + for (field, _, _) in EDITABLE_FIELDS { + if let Some(value) = matches.get_one::(*field) { + // πŸ‘‡ mappa language_book β†’ language + let db_field = if *field == "language_book" { + "language" + } else { + *field + }; + let val = value.trim(); + + // converte codice lingua in nome leggibile + let final_val = if *field == "language_book" { + lang_code_to_name(val).to_string() + } else { + val.to_string() + }; + + fields.insert(db_field.to_string(), final_val); + } + } + + if fields.is_empty() { + print_warn(&tr("edit.book.error_no_field")); + return Ok(()); + } + + // Heuristic: if contains letters, dash, or 13+ digits β†’ ISBN; otherwise ID + let is_isbn = key.len() >= 10 && !key.chars().all(|c| c.is_ascii_digit()); + + // Recupera i valori precedenti dal DB + let old_values = get_book_fields( + conn, + key, + &fields.keys().cloned().collect::>(), + is_isbn, + )?; + + let result = if is_isbn { + update_book_by_isbn(conn, key, &fields) + } else { + match key.parse::() { + Ok(id) => update_book_by_id(conn, id, &fields), + Err(_) => { + print_err(&tr("edit.book.error_invalid_id")); + return Ok(()); + } + } + }; + + match result { + Ok(rows) if rows > 0 => { + let mut modified_count = 0; + + // Confronta valori e stampa diff + for (field, new_val) in &fields { + let old_val = old_values.get(field).cloned().flatten(); + + match old_val { + Some(old) if old != *new_val => { + print_ok( + &tr_with( + "edit.field.updated", + &[("field", field), ("old", &old), ("new", new_val)], + ), + true, + ); + + modified_count += 1; + } + None => { + print_ok( + &tr_with("edit.field.set", &[("field", field), ("new", new_val)]), + true, + ); + + modified_count += 1; + } + Some(_) => { + print_info(&tr_with("edit.field.unchanged", &[("field", field)]), true); + } + } + } + + if modified_count > 0 { + let key_variant = if modified_count == 1 { + "edit.book.updated_one" + } else { + "edit.book.updated_many" + }; + + print_ok( + &tr_with( + key_variant, + &[("key", key), ("count", &modified_count.to_string())], + ), + true, + ); + } else { + print_warn(&tr("edit.book.no_changes")); + } + } + Ok(_) => print_warn(&tr("edit.book.not_found")), + Err(err) => print_err(&tr_with( + "edit.book.error_updating", + &[("error", &err.to_string())], + )), + } + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3534a55..7a3aefc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,6 +8,7 @@ pub mod add; pub mod add_book; pub mod backup; pub mod config; +pub mod edit_book; pub mod export; pub mod import; pub mod list; @@ -16,6 +17,7 @@ pub use add::handle_add; pub use add_book::handle_add_book; pub use backup::handle_backup; pub use config::handle_config; +pub use edit_book::handle_edit_book; pub use export::handle_export_csv; pub use export::handle_export_json; pub use export::handle_export_xlsx; diff --git a/src/db/books.rs b/src/db/books.rs new file mode 100644 index 0000000..b61a717 --- /dev/null +++ b/src/db/books.rs @@ -0,0 +1,94 @@ +use rusqlite::types::ValueRef; +use rusqlite::{Connection, Result, params, params_from_iter}; +use std::collections::HashMap; + +/// Costruisce la parte "SET col1 = ?, col2 = ?, ..." della query SQL +/// e restituisce anche il vettore dei valori corrispondenti. +/// +/// Esempio: +/// Input: {"title": "1984", "author": "Orwell"} +/// Output: ("SET title = ?, author = ?", vec!["1984", "Orwell"]) +fn build_update_clause(fields: &HashMap) -> (String, Vec) { + let mut sql = String::new(); + let mut params_vec = Vec::new(); + + for (i, (key, value)) in fields.iter().enumerate() { + sql.push_str(&format!("{} = ?", key)); + if i < fields.len() - 1 { + sql.push_str(", "); + } + params_vec.push(value.clone()); + } + + (sql, params_vec) +} + +pub fn update_book_by_id( + conn: &Connection, + id: i64, + fields: &HashMap, +) -> Result { + if fields.is_empty() { + return Ok(0); + } + + let (set_clause, mut params_vec) = build_update_clause(fields); + + let sql = format!("UPDATE books SET {} WHERE id = ?", set_clause); + params_vec.push(id.to_string()); + + let mut stmt = conn.prepare(&sql)?; + let rows_affected = stmt.execute(params_from_iter(params_vec.iter()))?; + Ok(rows_affected) +} + +pub fn update_book_by_isbn( + conn: &Connection, + isbn: &str, + fields: &HashMap, +) -> Result { + if fields.is_empty() { + return Ok(0); + } + + let (set_clause, mut params_vec) = build_update_clause(fields); + + let sql = format!("UPDATE books SET {} WHERE isbn = ?", set_clause); + params_vec.push(isbn.to_string()); + + let mut stmt = conn.prepare(&sql)?; + let rows_affected = stmt.execute(params_from_iter(params_vec.iter()))?; + Ok(rows_affected) +} + +/// Retrieve current values of the specified fields for a given book (by ID or ISBN). +pub fn get_book_fields( + conn: &Connection, + key: &str, + fields: &[String], + is_isbn: bool, +) -> Result>> { + let mut old_values: HashMap> = HashMap::new(); + + for field in fields { + let query = if is_isbn { + format!("SELECT {} FROM books WHERE isbn = ?", field) + } else { + format!("SELECT {} FROM books WHERE id = ?", field) + }; + + let result: Option = conn + .query_row(&query, params![key], |row| match row.get_ref(0)? { + ValueRef::Text(t) => Ok(Some(String::from_utf8_lossy(t).to_string())), + ValueRef::Integer(i) => Ok(Some(i.to_string())), + ValueRef::Real(f) => Ok(Some(f.to_string())), + ValueRef::Null => Ok(None), + _ => Ok(None), + }) + .unwrap_or_else(|_| None); + + old_values.insert(field.clone(), result); + } + + Ok(old_values) +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 695ccb9..98af129 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -8,8 +8,10 @@ //! models (title, author, year, isbn and a timestamp when the record was //! added). +pub mod books; pub mod load_db; pub mod migrate_db; +pub use books::{get_book_fields, update_book_by_id, update_book_by_isbn}; pub use load_db::{ensure_schema, init_db, start_db}; pub use migrate_db::{MigrationResult, run_migrations}; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9668b77..2d7079c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -133,5 +133,32 @@ "book.isbn.invalid_digit": "Invalid digit in ISBN number: {isbn}.", "book.isbn.digit_too_large": "Digit too large in ISBN number: {isbn}.", "book.isbn.invalid_group": "Invalid group identifier in ISBN: {isbn}.", - "book.isbn.undefined_range": "Undefined range in ISBN: {isbn}." + "book.isbn.undefined_range": "Undefined range in ISBN: {isbn}.", + "help.edit.about": "Edit existing records in the library database", + "help.edit.book.about": "Edit an existing book record by ID or ISBN", + "help.edit.book.key": "Specify the book ID or ISBN to edit", + "help.edit.book.title": "Update the book title", + "help.edit.book.author": "Update the author's name", + "help.edit.book.editor": "Update the publisher/editor name", + "help.edit.book.year": "Update the publication year", + "help.edit.book.lang_book": "Update the language of the book record", + "help.edit.book.genre": "Update the literary genre", + "help.edit.book.summary": "Update the book summary or description", + "help.edit.book.example.id": "Example: librius edit book 7 --title 'New Title'", + "help.edit.book.example.isbn": "Example: librius edit book 9788806239809 --author 'Umberto Eco'", + "help.edit.book.pages": "Update the number of pages", + "help.edit.book.room": "Update the room where the book is located", + "help.edit.book.shelf": "Update the shelf label", + "help.edit.book.row": "Update the row identifier", + "help.edit.book.position": "Update the position on the row", + "edit.field.updated": "Field β€œ{field}” updated successfully ({old} β†’ {new}).", + "edit.field.set": "Field β€œ{field}” set to {new}.", + "edit.field.unchanged": "Field β€œ{field}” unchanged (no modification).", + "edit.book.updated_one": "Book {key} successfully updated ({count} field modified).", + "edit.book.updated_many": "Book {key} successfully updated ({count} fields modified).", + "edit.book.no_changes": "No changes were applied.", + "edit.book.not_found": "No matching book found.", + "edit.book.error_updating": "Error updating book: {error}", + "edit.book.error_invalid_id": "Invalid ID format.", + "edit.book.error_no_field": "No fields specified to update." } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index c679729..14da2f1 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -133,5 +133,32 @@ "book.isbn.invalid_digit": "Cifra non valida nel numero ISBN: {isbn}.", "book.isbn.digit_too_large": "Cifra troppo grande nel numero ISBN: {isbn}.", "book.isbn.invalid_group": "Identificatore di gruppo non valido nell'ISBN: {isbn}.", - "book.isbn.undefined_range": "Intervallo indefinito nell'ISBN: {isbn}." + "book.isbn.undefined_range": "Intervallo indefinito nell'ISBN: {isbn}.", + "help.edit.about": "Modifica i record esistenti nel database della libreria", + "help.edit.book.about": "Modifica un libro esistente per ID o ISBN", + "help.edit.book.key": "Specifica l'ID o l'ISBN del libro da modificare", + "help.edit.book.title": "Aggiorna il titolo del libro", + "help.edit.book.author": "Aggiorna il nome dell'autore", + "help.edit.book.editor": "Aggiorna l'editore o la casa editrice", + "help.edit.book.year": "Aggiorna l'anno di pubblicazione", + "help.edit.book.lang_book": "Aggiorna la lingua del libro", + "help.edit.book.genre": "Aggiorna il genere letterario", + "help.edit.book.summary": "Aggiorna la descrizione o il riassunto del libro", + "help.edit.book.example.id": "Esempio: librius edit book 7 --title 'Nuovo titolo'", + "help.edit.book.example.isbn": "Esempio: librius edit book 9788806239809 --author 'Umberto Eco'", + "help.edit.book.pages": "Aggiorna il numero di pagine del libro", + "help.edit.book.room": "Aggiorna la stanza in cui si trova il libro", + "help.edit.book.shelf": "Aggiorna l'etichetta dello scaffale", + "help.edit.book.row": "Aggiorna l'identificativo di riga", + "help.edit.book.position": "Aggiorna la posizione nella riga", + "edit.field.updated": "Campo β€œ{field}” aggiornato correttamente ({old} β†’ {new}).", + "edit.field.set": "Campo β€œ{field}” impostato su {new}.", + "edit.field.unchanged": "Campo β€œ{field}” invariato (nessuna modifica).", + "edit.book.updated_one": "Libro {key} aggiornato correttamente ({count} campo modificato).", + "edit.book.updated_many": "Libro {key} aggiornato correttamente ({count} campi modificati).", + "edit.book.no_changes": "Nessuna modifica applicata.", + "edit.book.not_found": "Nessun libro corrispondente trovato.", + "edit.book.error_updating": "Errore durante l'aggiornamento del libro: {error}", + "edit.book.error_invalid_id": "Formato ID non valido.", + "edit.book.error_no_field": "Nessun campo specificato da aggiornare." } diff --git a/src/utils/fields.rs b/src/utils/fields.rs new file mode 100644 index 0000000..80954ad --- /dev/null +++ b/src/utils/fields.rs @@ -0,0 +1,14 @@ +pub const EDITABLE_FIELDS: &[(&str, &str, char)] = &[ + ("title", "help.edit.book.title", 't'), + ("author", "help.edit.book.author", 'a'), + ("editor", "help.edit.book.editor", 'e'), + ("year", "help.edit.book.year", 'y'), + ("language_book", "help.edit.book.lang_book", 'b'), + ("pages", "help.edit.book.pages", 'p'), + ("genre", "help.edit.book.genre", 'g'), + ("summary", "help.edit.book.summary", 's'), + ("room", "help.edit.book.room", 'r'), + ("shelf", "help.edit.book.shelf", 'f'), + ("row", "help.edit.book.row", 'w'), + ("position", "help.edit.book.position", 'o'), +]; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 174cf07..2a2550f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ // Contiene funzioni di supporto generali e costanti // grafiche per output CLI. // ===================================================== +pub mod fields; pub mod isbn; pub mod lang; pub mod table; From e2217acd1b299ae6a92ae9b87b5c239f3a2d0852 Mon Sep 17 00:00:00 2001 From: Alessandro Maestri Date: Mon, 20 Oct 2025 16:06:42 +0200 Subject: [PATCH 4/4] fix(clippy): fixed error during 'cargo clippy' check --- src/cli.rs | 2 +- src/commands/edit_book.rs | 2 +- src/db/books.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 513a31c..e04ee5d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -261,7 +261,7 @@ pub fn build_cli() -> Command { Arg::new(*name) .long(*name) .short(*short) - .help(tr_s(*help)) + .help(tr_s(help)) .num_args(1) .action(ArgAction::Set) .help_heading("Edit Book specific options") diff --git a/src/commands/edit_book.rs b/src/commands/edit_book.rs index fe34230..1170dc1 100644 --- a/src/commands/edit_book.rs +++ b/src/commands/edit_book.rs @@ -14,7 +14,7 @@ pub fn handle_edit_book(conn: &Connection, matches: &clap::ArgMatches) -> rusqli println!(); for (field, _, _) in EDITABLE_FIELDS { - if let Some(value) = matches.get_one::(*field) { + if let Some(value) = matches.get_one::(field) { // πŸ‘‡ mappa language_book β†’ language let db_field = if *field == "language_book" { "language" diff --git a/src/db/books.rs b/src/db/books.rs index b61a717..19476b7 100644 --- a/src/db/books.rs +++ b/src/db/books.rs @@ -85,7 +85,7 @@ pub fn get_book_fields( ValueRef::Null => Ok(None), _ => Ok(None), }) - .unwrap_or_else(|_| None); + .unwrap_or(None); old_values.insert(field.clone(), result); }