Database trait and JSON implementation (#6)

Closes #3

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/6
This commit is contained in:
Wojciech Kozlowski 2023-03-29 08:37:01 +02:00
parent 4de22e5584
commit c40b29cd27
6 changed files with 444 additions and 11 deletions

213
Cargo.lock generated
View File

@ -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"

View File

@ -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"

197
src/database/json.rs Normal file
View File

@ -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<Self, std::io::Error> {
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<Self, std::io::Error> {
Ok(Self {
database_file: File::create(path)?,
})
}
}
impl DatabaseRead for DatabaseJson {
fn read<D>(&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<S>(&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<ReleaseGroup> {
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<ReleaseGroup> = 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<ReleaseGroup> = 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);
}
}

18
src/database/mod.rs Normal file
View File

@ -0,0 +1,18 @@
use serde::de::DeserializeOwned;
use serde::Serialize;
pub mod json;
/// Trait for database reads.
pub trait DatabaseRead {
fn read<D>(&mut self, collection: &mut D) -> Result<(), std::io::Error>
where
D: DeserializeOwned;
}
/// Trait for database writes.
pub trait DatabaseWrite {
fn write<S>(&mut self, collection: &S) -> Result<(), std::io::Error>
where
S: Serialize;
}

View File

@ -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<Mbid>,
}
/// [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<Artist>,
pub mbid: Option<Mbid>,
}
/// [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<Track>,
pub mbid: Option<Mbid>,
}
/// [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<Artist>,
pub year: u32,
pub mbid: Option<Mbid>,
pub releases: Vec<Release>,

View File

@ -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"}]}]