use std::io::{BufRead, Read, Seek, Write};
use super::file_format::{FileFormatReader, FileFormatWriter};
use super::{IoReadError, IoWriteError};
use crate::{Collection, Puzzle};
pub struct JsonReader {}
impl JsonReader {
pub fn new() -> Self { JsonReader {} }
}
impl FileFormatReader for JsonReader {
fn read<R>(&mut self, src: &mut R) -> Result<Collection, IoReadError>
where
R: Read + BufRead + Seek,
{
let mut data = String::new();
src.read_to_string(&mut data)?;
serde_json::from_str(&data).map_err(IoReadError::new_json_syntax_error)
}
}
pub struct JsonWriter {}
impl JsonWriter {
pub fn new() -> Self { JsonWriter {} }
}
impl FileFormatWriter for JsonWriter {
fn write_collection<W>(&self, collection: &Collection, dest: &mut W) -> Result<(), IoWriteError>
where
W: Write,
{
Self::write_json(collection, 0, dest)
}
fn write_puzzle<W>(
&self, puzzle: &Puzzle, puzzle_idx: usize, dest: &mut W,
) -> Result<(), IoWriteError>
where
W: Write,
{
Self::write_json(puzzle, puzzle_idx, dest)
}
}
impl JsonWriter {
fn write_json<W, T>(data: &T, puzzle_idx: usize, dest: &mut W) -> Result<(), IoWriteError>
where
W: Write,
T: serde::Serialize,
{
serde_json::to_writer_pretty(&mut *dest, data)
.map_err(|e| IoWriteError::new_io_error(e.into(), puzzle_idx))?;
dest.write_all(b"\n")
.map_err(|e| IoWriteError::new_io_error(e, puzzle_idx))
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::path::PathBuf;
use indoc::indoc;
use tempfile::NamedTempFile;
use super::super::IoReadErrorKind;
use super::super::sok_file_format::SokReader;
use super::*;
use crate::{SolvingMode, Tessellation};
macro_rules! tests_data_dir {
() => {
concat!(env!("CARGO_MANIFEST_DIR"), "/src/io/tests_data/")
};
}
fn collection_from(file_name: &str) -> Result<Collection, Box<dyn Error>> {
let src_file_path = PathBuf::from(tests_data_dir!()).join(file_name);
let mut file = BufReader::new(File::open(src_file_path)?);
if file_name.ends_with(".json") {
Ok(JsonReader::new().read(&mut file)?)
} else {
Ok(SokReader::new(None, false).read(&mut file)?)
}
}
#[test]
fn it_reads_collection_from_file() -> Result<(), Box<dyn Error>> {
let collection = collection_from("small_collection.json")?;
assert_eq!(collection.title, "Mixed collection");
assert_eq!(collection.author, "ZOMG! Collection author");
assert_eq!(collection.created_at, "2022-07-01 01:05:06");
assert_eq!(collection.puzzles.len(), 3);
let puzzle = &collection.puzzles[0];
assert_eq!(puzzle.title, "Sokoban Level 1");
assert_eq!(puzzle.board.tessellation(), Tessellation::Sokoban);
assert_eq!(puzzle.board.width(), 19);
assert_eq!(puzzle.saved_games.len(), 3);
assert_eq!(puzzle.saved_games[2].solving_mode(), SolvingMode::Reverse);
let sokoban_plus = collection.puzzles[1]
.sokoban_plus
.as_ref()
.ok_or("puzzle 1 should carry Sokoban+")?;
assert_eq!(sokoban_plus.boxorder(), "1 2 3 4 5");
assert_eq!(sokoban_plus.goalorder(), "3 4 5 2 1");
assert_eq!(collection.puzzles[2].board.tessellation(), Tessellation::Hexoban);
Ok(())
}
#[test]
fn it_writes_collection_to_file() -> Result<(), Box<dyn Error>> {
let collection1 = collection_from("small_collection.json")?;
let collection2 = {
let mut file1 = NamedTempFile::new()?;
let mut file2 = BufReader::new(file1.reopen()?);
JsonWriter::new().write_collection(&collection1, &mut file1)?;
JsonReader::new().read(&mut file2)?
};
assert_eq!(collection1, collection2);
Ok(())
}
#[test]
fn it_agrees_with_the_sok_reader() -> Result<(), Box<dyn Error>> {
assert_eq!(
collection_from("small_collection.json")?,
collection_from("small_collection.sok")?
);
Ok(())
}
#[test]
fn when_json_is_malformed_it_reports_the_line() {
let data = indoc! {r#"
{
"title": "Broken",
"puzzles": [,]
}
"#};
let err = JsonReader::new()
.read(&mut Cursor::new(data.as_bytes()))
.expect_err("malformed JSON must be rejected");
assert!(
matches!(err.kind(), IoReadErrorKind::JsonSyntaxError { ln } if *ln == 3),
"{:?}",
err.kind()
);
}
#[test]
fn when_a_board_does_not_parse_it_is_reported_as_a_json_error() -> Result<(), Box<dyn Error>> {
let data = r##"{"puzzles": [{"board": {"tessellation": "Hexoban", "board": "#-##-#"}}]}"##;
let err = JsonReader::new()
.read(&mut Cursor::new(data.as_bytes()))
.expect_err("an unparsable board must be rejected");
assert!(matches!(err.kind(), IoReadErrorKind::JsonSyntaxError { .. }));
let source = err
.source()
.and_then(|source| source.downcast_ref::<serde_json::Error>())
.ok_or("source should be a serde_json error")?;
assert_eq!(source.classify(), serde_json::error::Category::Data);
Ok(())
}
#[test]
fn when_json_is_a_foreign_document_it_reads_as_an_empty_collection()
-> Result<(), Box<dyn Error>> {
let collection = JsonReader::new().read(&mut Cursor::new(b"{}"))?;
assert_eq!(collection, Collection::new());
Ok(())
}
}