Decouple DatabaseJson from files #11
@ -1,46 +1,39 @@
|
|||||||
//! Module for storing MusicHoard data in a JSON file database.
|
//! Module for storing MusicHoard data in a JSON file database.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::{read_to_string, write};
|
||||||
use std::io::{Read, Write};
|
use std::path::PathBuf;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::{DatabaseRead, DatabaseWrite};
|
use super::{DatabaseRead, DatabaseWrite};
|
||||||
|
|
||||||
|
/// Trait for the JSON database backend.
|
||||||
|
pub trait DatabaseJsonBackend {
|
||||||
|
/// Read the JSON string from the backend.
|
||||||
|
fn read(&self) -> Result<String, std::io::Error>;
|
||||||
|
|
||||||
|
/// Write the JSON string to the backend.
|
||||||
|
fn write(&mut self, json: &str) -> Result<(), std::io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
/// A JSON file database.
|
/// A JSON file database.
|
||||||
pub struct DatabaseJson {
|
pub struct DatabaseJson {
|
||||||
database_file: File,
|
backend: Box<dyn DatabaseJsonBackend>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DatabaseJson {
|
impl DatabaseJson {
|
||||||
/// Create a read-only database instance. If the JSON file does not exist, an error is returned.
|
pub fn new(backend: Box<dyn DatabaseJsonBackend>) -> Self {
|
||||||
pub fn reader(path: &Path) -> Result<Self, std::io::Error> {
|
DatabaseJson { backend }
|
||||||
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 {
|
impl DatabaseRead for DatabaseJson {
|
||||||
fn read<D>(&mut self, collection: &mut D) -> Result<(), std::io::Error>
|
fn read<D>(&self, collection: &mut D) -> Result<(), std::io::Error>
|
||||||
where
|
where
|
||||||
D: DeserializeOwned,
|
D: DeserializeOwned,
|
||||||
{
|
{
|
||||||
// Read entire file to memory as for now this is faster than a buffered read from disk:
|
let serialized = self.backend.read()?;
|
||||||
// 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)?;
|
*collection = serde_json::from_str(&serialized)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -52,21 +45,54 @@ impl DatabaseWrite for DatabaseJson {
|
|||||||
S: Serialize,
|
S: Serialize,
|
||||||
{
|
{
|
||||||
let serialized = serde_json::to_string(&collection)?;
|
let serialized = serde_json::to_string(&collection)?;
|
||||||
self.database_file.write_all(serialized.as_bytes())?;
|
self.backend.write(&serialized)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DatabaseJsonFile {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseJsonFile {
|
||||||
|
/// Create a database instance that will read/write to the provided path.
|
||||||
|
pub fn new(path: PathBuf) -> Self {
|
||||||
|
DatabaseJsonFile { path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseJsonBackend for DatabaseJsonFile {
|
||||||
|
fn read(&self) -> Result<String, std::io::Error> {
|
||||||
|
// 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
|
||||||
|
read_to_string(&self.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
|
||||||
|
write(&self.path, json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::{Album, AlbumId, Track};
|
use crate::{Album, AlbumId, Track};
|
||||||
|
|
||||||
const TEST_FILENAME: &str = "tests/files/database_json_test.json";
|
struct DatabaseJsonTest {
|
||||||
|
json: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseJsonBackend for DatabaseJsonTest {
|
||||||
|
fn read(&self) -> Result<String, std::io::Error> {
|
||||||
|
Ok(self.json.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
|
||||||
|
assert_eq!(self.json, json);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn test_data() -> Vec<Album> {
|
fn test_data() -> Vec<Album> {
|
||||||
vec![
|
vec![
|
||||||
@ -109,55 +135,91 @@ mod tests {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn album_to_json(album: &Album) -> String {
|
||||||
|
let album_id_artist = &album.id.artist;
|
||||||
|
let album_id_year = album.id.year;
|
||||||
|
let album_id_title = &album.id.title;
|
||||||
|
|
||||||
|
let mut album_tracks: Vec<String> = vec![];
|
||||||
|
for track in album.tracks.iter() {
|
||||||
|
let track_number = track.number;
|
||||||
|
let track_title = &track.title;
|
||||||
|
|
||||||
|
let mut track_artist: Vec<String> = vec![];
|
||||||
|
for artist in track.artist.iter() {
|
||||||
|
track_artist.push(format!("\"{artist}\""))
|
||||||
|
}
|
||||||
|
let track_artist = track_artist.join(",");
|
||||||
|
|
||||||
|
album_tracks.push(format!(
|
||||||
|
"{{\
|
||||||
|
\"number\":{track_number},\
|
||||||
|
\"title\":\"{track_title}\",\
|
||||||
|
\"artist\":[{track_artist}]\
|
||||||
|
}}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let album_tracks = album_tracks.join(",");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{{\
|
||||||
|
\"id\":{{\
|
||||||
|
\"artist\":\"{album_id_artist}\",\
|
||||||
|
\"year\":{album_id_year},\
|
||||||
|
\"title\":\"{album_id_title}\"\
|
||||||
|
}},\"tracks\":[{album_tracks}]\
|
||||||
|
}}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn albums_to_json(albums: &Vec<Album>) -> String {
|
||||||
|
let mut albums_strings: Vec<String> = vec![];
|
||||||
|
for album in albums.iter() {
|
||||||
|
albums_strings.push(album_to_json(album));
|
||||||
|
}
|
||||||
|
let albums_json = albums_strings.join(",");
|
||||||
|
format!("[{albums_json}]")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write() {
|
fn write() {
|
||||||
let write_data = test_data();
|
let write_data = test_data();
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
let backend = DatabaseJsonTest {
|
||||||
DatabaseJson::writer(temp_file.path())
|
json: albums_to_json(&write_data),
|
||||||
.unwrap()
|
};
|
||||||
|
|
||||||
|
DatabaseJson::new(Box::new(backend))
|
||||||
.write(&write_data)
|
.write(&write_data)
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn read() {
|
fn read() {
|
||||||
|
let expected = test_data();
|
||||||
|
let backend = DatabaseJsonTest {
|
||||||
|
json: albums_to_json(&expected),
|
||||||
|
};
|
||||||
|
|
||||||
let mut read_data: Vec<Album> = vec![];
|
let mut read_data: Vec<Album> = vec![];
|
||||||
DatabaseJson::reader(Path::new(TEST_FILENAME))
|
DatabaseJson::new(Box::new(backend))
|
||||||
.unwrap()
|
|
||||||
.read(&mut read_data)
|
.read(&mut read_data)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(read_data, test_data());
|
|
||||||
|
assert_eq!(read_data, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reverse() {
|
fn reverse() {
|
||||||
|
let expected = test_data();
|
||||||
|
let backend = DatabaseJsonTest {
|
||||||
|
json: albums_to_json(&expected),
|
||||||
|
};
|
||||||
|
let mut database = DatabaseJson::new(Box::new(backend));
|
||||||
|
|
||||||
let write_data = test_data();
|
let write_data = test_data();
|
||||||
let mut read_data: Vec<Album> = vec![];
|
let mut read_data: Vec<Album> = vec![];
|
||||||
|
database.write(&write_data).unwrap();
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
database.read(&mut read_data).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);
|
assert_eq!(write_data, read_data);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ pub mod json;
|
|||||||
/// Trait for database reads.
|
/// Trait for database reads.
|
||||||
pub trait DatabaseRead {
|
pub trait DatabaseRead {
|
||||||
/// Read collection from the database.
|
/// Read collection from the database.
|
||||||
fn read<D>(&mut self, collection: &mut D) -> Result<(), std::io::Error>
|
fn read<D>(&self, collection: &mut D) -> Result<(), std::io::Error>
|
||||||
where
|
where
|
||||||
D: DeserializeOwned;
|
D: DeserializeOwned;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ impl QueryArgsBeets for Query {
|
|||||||
/// Trait for invoking beets commands.
|
/// Trait for invoking beets commands.
|
||||||
pub trait BeetsExecutor {
|
pub trait BeetsExecutor {
|
||||||
/// Invoke beets with the provided arguments.
|
/// Invoke beets with the provided arguments.
|
||||||
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error>;
|
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Struct for interacting with the music library via beets.
|
/// Struct for interacting with the music library via beets.
|
||||||
@ -106,7 +106,7 @@ impl Beets {
|
|||||||
impl Library for Beets {
|
impl Library for Beets {
|
||||||
fn list(&mut self, query: &Query) -> Result<Vec<Album>, Error> {
|
fn list(&mut self, query: &Query) -> Result<Vec<Album>, Error> {
|
||||||
let cmd = Self::list_cmd_and_args(query);
|
let cmd = Self::list_cmd_and_args(query);
|
||||||
let output = self.executor.exec(cmd)?;
|
let output = self.executor.exec(&cmd)?;
|
||||||
Self::list_to_albums(output)
|
Self::list_to_albums(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,7 +209,7 @@ impl Default for SystemExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsExecutor for SystemExecutor {
|
impl BeetsExecutor for SystemExecutor {
|
||||||
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error> {
|
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error> {
|
||||||
let output = Command::new(&self.bin).args(arguments).output()?;
|
let output = Command::new(&self.bin).args(arguments).output()?;
|
||||||
let output = std::str::from_utf8(&output.stdout)?;
|
let output = std::str::from_utf8(&output.stdout)?;
|
||||||
Ok(output.split('\n').map(|s| s.to_string()).collect())
|
Ok(output.split('\n').map(|s| s.to_string()).collect())
|
||||||
@ -226,10 +226,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsExecutor for TestExecutor {
|
impl BeetsExecutor for TestExecutor {
|
||||||
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error> {
|
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error> {
|
||||||
if self.arguments.is_some() {
|
assert_eq!(&self.arguments.take().unwrap(), arguments);
|
||||||
assert_eq!(self.arguments.take().unwrap(), arguments);
|
|
||||||
}
|
|
||||||
self.output.take().unwrap()
|
self.output.take().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,6 +301,14 @@ mod tests {
|
|||||||
strings
|
strings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn albums_to_beets_string(albums: &Vec<Album>) -> Vec<String> {
|
||||||
|
let mut strings = vec![];
|
||||||
|
for album in albums.iter() {
|
||||||
|
strings.append(&mut album_to_beets_string(album));
|
||||||
|
}
|
||||||
|
strings
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_query() {
|
fn test_query() {
|
||||||
let query = Query::new()
|
let query = Query::new()
|
||||||
@ -341,10 +347,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_list_ordered() {
|
fn test_list_ordered() {
|
||||||
let expected = test_data();
|
let expected = test_data();
|
||||||
let mut output = vec![];
|
let output = albums_to_beets_string(&expected);
|
||||||
for album in expected.iter() {
|
|
||||||
output.append(&mut album_to_beets_string(album));
|
|
||||||
}
|
|
||||||
|
|
||||||
let executor = TestExecutor {
|
let executor = TestExecutor {
|
||||||
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
|
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
|
||||||
@ -359,10 +362,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_list_unordered() {
|
fn test_list_unordered() {
|
||||||
let mut expected = test_data();
|
let mut expected = test_data();
|
||||||
let mut output = vec![];
|
let mut output = albums_to_beets_string(&expected);
|
||||||
for album in expected.iter() {
|
|
||||||
output.append(&mut album_to_beets_string(album));
|
|
||||||
}
|
|
||||||
let last = output.len() - 1;
|
let last = output.len() - 1;
|
||||||
output.swap(0, last);
|
output.swap(0, last);
|
||||||
|
|
||||||
@ -391,10 +391,7 @@ mod tests {
|
|||||||
expected[1].id.year = expected[0].id.year;
|
expected[1].id.year = expected[0].id.year;
|
||||||
expected[1].id.title = expected[0].id.title.clone();
|
expected[1].id.title = expected[0].id.title.clone();
|
||||||
|
|
||||||
let mut output = vec![];
|
let output = albums_to_beets_string(&expected);
|
||||||
for album in expected.iter() {
|
|
||||||
output.append(&mut album_to_beets_string(album));
|
|
||||||
}
|
|
||||||
|
|
||||||
let executor = TestExecutor {
|
let executor = TestExecutor {
|
||||||
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
|
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
|
||||||
|
@ -1 +0,0 @@
|
|||||||
[{"id":{"artist":"Artist A","year":1998,"title":"Release group A"},"tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"id":{"artist":"Artist B","year":2008,"title":"Release group B"},"tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}]
|
|
Loading…
Reference in New Issue
Block a user