From 42a41feeadbc998936cb8dee563d18d4adf3025a Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 29 Mar 2023 08:37:01 +0200 Subject: [PATCH] 3---database-trait-and-json-implementation (#6) Closes #3 Co-authored-by: Wojciech Kozlowski Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/6 --- Cargo.lock | 213 ++++++++++++++++++++++++++++ Cargo.toml | 4 + src/database/json.rs | 197 +++++++++++++++++++++++++ src/database/mod.rs | 18 +++ src/lib.rs | 22 +-- tests/files/database_json_test.json | 1 + 6 files changed, 444 insertions(+), 11 deletions(-) create mode 100644 src/database/json.rs create mode 100644 src/database/mod.rs create mode 100644 tests/files/database_json_test.json diff --git a/Cargo.lock b/Cargo.lock index 08d3641..d102cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,105 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "linux-raw-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" + [[package]] name = "musichoard" version = "0.1.0" dependencies = [ "serde", + "serde_json", + "tempfile", "uuid", ] @@ -28,6 +122,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.37.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c348b5dc624ecee40108aa2922fed8bad89d7fcc2b9f8cb18f632898ac4a37f9" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "serde" version = "1.0.159" @@ -48,6 +171,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "syn" version = "2.0.11" @@ -59,6 +193,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.8" @@ -73,3 +220,69 @@ checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "serde", ] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml index 03065c7..306828f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,8 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" uuid = { version = "1.3", features = ["serde"] } + +[dev-dependencies] +tempfile = "3.5" diff --git a/src/database/json.rs b/src/database/json.rs new file mode 100644 index 0000000..7807a90 --- /dev/null +++ b/src/database/json.rs @@ -0,0 +1,197 @@ +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; + +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::database::{DatabaseRead, DatabaseWrite}; + +/// A JSON file database. +pub struct DatabaseJson { + database_file: File, +} + +impl DatabaseJson { + /// Create a read-only database instance. If the JSON file does not exist, an error is returned. + pub fn reader(path: &Path) -> Result { + Ok(Self { + database_file: File::open(path)?, + }) + } + + /// Create a write-only database instance. If the file does not exist, it is created, if it does + /// exist, it is truncated. + pub fn writer(path: &Path) -> Result { + Ok(Self { + database_file: File::create(path)?, + }) + } +} + +impl DatabaseRead for DatabaseJson { + fn read(&mut self, collection: &mut D) -> Result<(), std::io::Error> + where + D: DeserializeOwned, + { + // Read entire file to memory as for now this is faster than a buffered read from disk: + // https://github.com/serde-rs/json/issues/160 + let mut serialized = String::new(); + self.database_file.read_to_string(&mut serialized)?; + + *collection = serde_json::from_str(&serialized)?; + Ok(()) + } +} + +impl DatabaseWrite for DatabaseJson { + fn write(&mut self, collection: &S) -> Result<(), std::io::Error> + where + S: Serialize, + { + let serialized = serde_json::to_string(&collection)?; + self.database_file.write_all(serialized.as_bytes())?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use tempfile::NamedTempFile; + use uuid::uuid; + + use super::*; + + use crate::{Artist, Release, ReleaseGroup, ReleaseGroupType, Track}; + + const TEST_FILENAME: &str = "tests/files/database_json_test.json"; + + fn test_data() -> Vec { + vec![ + ReleaseGroup { + r#type: ReleaseGroupType::Album, + title: String::from("Release group A"), + artist: vec![Artist { + name: String::from("Artist A"), + mbid: Some(uuid!("f7769831-746b-4a12-8124-0123d7fe17c9")), + }], + year: 1998, + mbid: Some(uuid!("89efbf43-3395-4f6e-ac11-32c1ce514bb0")), + releases: vec![Release { + tracks: vec![ + Track { + number: 1, + title: String::from("Track A.1"), + artist: vec![Artist { + name: String::from("Artist A.A"), + mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), + }], + mbid: None, + }, + Track { + number: 2, + title: String::from("Track A.2"), + artist: vec![Artist { + name: String::from("Artist A.A"), + mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), + }], + mbid: None, + }, + Track { + number: 3, + title: String::from("Track A.3"), + artist: vec![ + Artist { + name: String::from("Artist A.A"), + mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), + }, + Artist { + name: String::from("Artist A.B"), + mbid: Some(uuid!("6f6b46f2-4bb5-47e7-a8c8-03ebde30164f")), + }, + ], + mbid: None, + }, + ], + mbid: None, + }], + }, + ReleaseGroup { + r#type: ReleaseGroupType::Single, + title: String::from("Release group B"), + artist: vec![Artist { + name: String::from("Artist B"), + mbid: None, + }], + year: 2008, + mbid: None, + releases: vec![Release { + tracks: vec![Track { + number: 1, + title: String::from("Track B.1"), + artist: vec![Artist { + name: String::from("Artist B.A"), + mbid: Some(uuid!("d927e216-2e63-415c-acec-bf9f1abd3e3c")), + }], + mbid: Some(uuid!("dacc9ce4-118c-4c92-aed7-1ebe4c7543b5")), + }], + mbid: Some(uuid!("ac7b642d-8b71-4588-a694-e5ae43fac873")), + }], + }, + ] + } + + #[test] + fn write() { + let write_data = test_data(); + let temp_file = NamedTempFile::new().unwrap(); + DatabaseJson::writer(temp_file.path()) + .unwrap() + .write(&write_data) + .unwrap(); + + let mut write_data_str = String::new(); + File::open(temp_file.path()) + .unwrap() + .read_to_string(&mut write_data_str) + .unwrap(); + + let mut test_data_str = String::new(); + File::open(TEST_FILENAME) + .unwrap() + .read_to_string(&mut test_data_str) + .unwrap(); + + assert_eq!(write_data_str, test_data_str); + } + + #[test] + fn read() { + let mut read_data: Vec = vec![]; + DatabaseJson::reader(Path::new(TEST_FILENAME)) + .unwrap() + .read(&mut read_data) + .unwrap(); + assert_eq!(read_data, test_data()); + } + + #[test] + fn reverse() { + let write_data = test_data(); + let mut read_data: Vec = vec![]; + + let temp_file = NamedTempFile::new().unwrap(); + + DatabaseJson::writer(temp_file.path()) + .unwrap() + .write(&write_data) + .unwrap(); + DatabaseJson::reader(temp_file.path()) + .unwrap() + .read(&mut read_data) + .unwrap(); + + assert_eq!(write_data, read_data); + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..5dff0b3 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,18 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub mod json; + +/// Trait for database reads. +pub trait DatabaseRead { + fn read(&mut self, collection: &mut D) -> Result<(), std::io::Error> + where + D: DeserializeOwned; +} + +/// Trait for database writes. +pub trait DatabaseWrite { + fn write(&mut self, collection: &S) -> Result<(), std::io::Error> + where + S: Serialize; +} diff --git a/src/lib.rs b/src/lib.rs index 2fa4bc8..88c424c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,38 @@ //! MusicHoard - a music collection manager. use serde::{Deserialize, Serialize}; -use uuid; +use uuid::Uuid; + +pub mod database; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). -pub type Mbid = uuid::Uuid; +pub type Mbid = Uuid; /// [Artist](https://musicbrainz.org/doc/Artist). -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct Artist { pub name: String, pub mbid: Option, } /// [Track](https://musicbrainz.org/doc/Track). -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct Track { pub number: u32, pub title: String, - pub artist: Artist, + pub artist: Vec, pub mbid: Option, } /// [Release](https://musicbrainz.org/doc/Release). -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct Release { - pub title: String, - pub artist: String, pub tracks: Vec, pub mbid: Option, } /// [Release group primary type](https://musicbrainz.org/doc/Release_Group/Type). -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub enum ReleaseGroupType { Album, Ep, @@ -41,11 +41,11 @@ pub enum ReleaseGroupType { } /// [Release group](https://musicbrainz.org/doc/Release_Group). -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct ReleaseGroup { pub r#type: ReleaseGroupType, pub title: String, - pub artist: Artist, + pub artist: Vec, pub year: u32, pub mbid: Option, pub releases: Vec, diff --git a/tests/files/database_json_test.json b/tests/files/database_json_test.json new file mode 100644 index 0000000..4e3f7fc --- /dev/null +++ b/tests/files/database_json_test.json @@ -0,0 +1 @@ +[{"type":"Album","title":"Release group A","artist":[{"name":"Artist A","mbid":"f7769831-746b-4a12-8124-0123d7fe17c9"}],"year":1998,"mbid":"89efbf43-3395-4f6e-ac11-32c1ce514bb0","releases":[{"tracks":[{"number":1,"title":"Track A.1","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"}],"mbid":null},{"number":2,"title":"Track A.2","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"}],"mbid":null},{"number":3,"title":"Track A.3","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"},{"name":"Artist A.B","mbid":"6f6b46f2-4bb5-47e7-a8c8-03ebde30164f"}],"mbid":null}],"mbid":null}]},{"type":"Single","title":"Release group B","artist":[{"name":"Artist B","mbid":null}],"year":2008,"mbid":null,"releases":[{"tracks":[{"number":1,"title":"Track B.1","artist":[{"name":"Artist B.A","mbid":"d927e216-2e63-415c-acec-bf9f1abd3e3c"}],"mbid":"dacc9ce4-118c-4c92-aed7-1ebe4c7543b5"}],"mbid":"ac7b642d-8b71-4588-a694-e5ae43fac873"}]}] \ No newline at end of file