From 30f38958e92fe5b5bf447d12e35946858789ca51 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 20 Oct 2023 12:15:45 -0700 Subject: [PATCH] port: Kludge together `WhenceUniverse::save()` support, for native JSON only. --- all-is-cubes-port/src/file.rs | 31 +++++++++++++++--- all-is-cubes-port/src/lib.rs | 38 ++++++++++++++++------ all-is-cubes-port/src/native.rs | 12 +++---- all-is-cubes-port/src/tests.rs | 56 +++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 24 deletions(-) diff --git a/all-is-cubes-port/src/file.rs b/all-is-cubes-port/src/file.rs index 58e3b477d..785e49a29 100644 --- a/all-is-cubes-port/src/file.rs +++ b/all-is-cubes-port/src/file.rs @@ -19,6 +19,13 @@ pub trait Fileish: fmt::Debug + Send + Sync { /// /// TODO: This should probably be async. fn read(&self) -> Result, io::Error>; + + /// Overwrites the file contents, if possible. + /// + /// TODO: This should probably be async. + /// + /// TODO: Need a way to communicate “this is definitely never writability”. + fn write(&self, data: &[u8]) -> Result<(), io::Error>; } // TODO: when Rust has generic associated types we will no longer @@ -40,17 +47,23 @@ impl Fileish for PathBuf { fn read(&self) -> Result, io::Error> { std::fs::read(self) } + + fn write(&self, data: &[u8]) -> Result<(), io::Error> { + std::fs::write(self, data) + } } /// General-purpose implementation of [`Fileish`]. +/// +/// TODO: figure out how writing works for this pub struct NonDiskFile { name: String, - opener: O, + reader: O, } impl fmt::Debug for NonDiskFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { name, opener: _ } = self; + let Self { name, reader: _ } = self; f.debug_struct("NonDiskFile") .field("name", name) .finish_non_exhaustive() @@ -60,7 +73,10 @@ impl fmt::Debug for NonDiskFile { impl NonDiskFile { /// Construct a new [`NonDiskFile`] from its parts. pub fn from_name_and_data_source(name: String, opener: O) -> Self { - Self { name, opener } + Self { + name, + reader: opener, + } } } @@ -77,6 +93,13 @@ where } fn read(&self) -> Result, io::Error> { - (self.opener)() + (self.reader)() + } + + fn write(&self, _data: &[u8]) -> Result<(), io::Error> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "writing is not yet implemented for NonDiskFile", + )) } } diff --git a/all-is-cubes-port/src/lib.rs b/all-is-cubes-port/src/lib.rs index d67b405ee..076be2d26 100644 --- a/all-is-cubes-port/src/lib.rs +++ b/all-is-cubes-port/src/lib.rs @@ -48,9 +48,9 @@ #![warn(missing_docs)] use std::ffi::OsString; -use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::{fs, io}; use futures_core::future::BoxFuture; @@ -123,7 +123,10 @@ pub async fn export_to_path( destination: PathBuf, ) -> Result<(), crate::ExportError> { match format { - ExportFormat::AicJson => native::export_native_json(progress, source, destination).await, + ExportFormat::AicJson => { + let mut writer = io::BufWriter::new(fs::File::create(destination)?); + native::export_native_json(progress, source, &mut writer).await + } ExportFormat::DotVox => { // TODO: async file IO? mv::export_dot_vox(progress, source, fs::File::create(destination)?).await @@ -216,10 +219,11 @@ impl all_is_cubes::save::WhenceUniverse for PortWhence { } fn can_save(&self) -> bool { - // TODO: implement this along with save() - #[allow(unused)] - let _ = self.save_format; - false + match self.save_format { + Some(ExportFormat::AicJson) => true, + Some(_) => false, + None => false, + } } fn load( @@ -235,10 +239,24 @@ impl all_is_cubes::save::WhenceUniverse for PortWhence { universe: &Universe, progress: YieldProgress, ) -> BoxFuture<'static, Result<(), Box>> { - // TODO: in order to implement this we need to be able to write to a `Fileish` - // or have an accompanying destination - let _ = (universe, progress, self.save_format); - Box::pin(async { Err("saving via `WhenceUniverse` is not yet implemented".into()) }) + let source = ExportSet::all_of_universe(universe); + let save_format = self.save_format; + let file = self.file.clone(); + Box::pin(async move { + // TODO: merge this and `export_to_path()` + match save_format { + Some(ExportFormat::AicJson) => { + let mut buf = Vec::new(); + native::export_native_json(progress, source, &mut buf).await?; + file.write(&buf)?; + Ok(()) + } + Some(_) => { + Err("saving this format via `WhenceUniverse` is not yet implemented".into()) + } + None => Err("saving the file format that was loaded is not supported".into()), + } + }) } } diff --git a/all-is-cubes-port/src/native.rs b/all-is-cubes-port/src/native.rs index 6112df92b..29bb03fbb 100644 --- a/all-is-cubes-port/src/native.rs +++ b/all-is-cubes-port/src/native.rs @@ -1,5 +1,4 @@ -use std::path::PathBuf; -use std::{fs, io}; +use std::io; use all_is_cubes::universe::Universe; use all_is_cubes::util::YieldProgress; @@ -29,18 +28,15 @@ pub(crate) fn import_native_json( }) } +/// The `destination` should be buffered for efficiency. pub(crate) async fn export_native_json( progress: YieldProgress, source: ExportSet, - destination: PathBuf, + destination: &mut (dyn io::Write + Send), ) -> Result<(), ExportError> { // TODO: Spin off a blocking thread to perform this export let ExportSet { contents } = source; - serde_json::to_writer( - io::BufWriter::new(fs::File::create(destination)?), - &contents, - ) - .map_err(|error| { + serde_json::to_writer(destination, &contents).map_err(|error| { // TODO: report non-IO errors distinctly ExportError::Write(io::Error::new(io::ErrorKind::Other, error)) })?; diff --git a/all-is-cubes-port/src/tests.rs b/all-is-cubes-port/src/tests.rs index ba1fb027f..a477a7eed 100644 --- a/all-is-cubes-port/src/tests.rs +++ b/all-is-cubes-port/src/tests.rs @@ -1,12 +1,17 @@ use std::error::Error as _; +use std::fs; use std::sync::Arc; -use all_is_cubes::block; +use pretty_assertions::assert_eq; +use serde_json::json; + +use all_is_cubes::block::{self, AIR}; use all_is_cubes::util::{assert_send_sync, yield_progress_for_testing}; use crate::file::NonDiskFile; use crate::{ - load_universe_from_file, BlockDef, ExportError, ExportSet, ImportError, Path, PathBuf, Universe, + export_to_path, load_universe_from_file, BlockDef, ExportError, ExportFormat, ExportSet, + ImportError, Path, PathBuf, Universe, }; #[test] @@ -54,3 +59,50 @@ fn member_export_path() { PathBuf::from("/export/data.ext"), ); } + +#[tokio::test] +async fn port_whence_load_then_save() { + let tmp_dir = tempfile::tempdir().unwrap(); + let path: PathBuf = tmp_dir.path().join("foo.alliscubesjson"); + + // Write initial state. + export_to_path( + yield_progress_for_testing(), + ExportFormat::AicJson, + ExportSet::all_of_universe(&Universe::new()), + path.clone(), + ) + .await + .unwrap(); + + // Load it again, producing a universe containing `PortWhence`. + let mut universe = + load_universe_from_file(yield_progress_for_testing(), Arc::new(path.clone())) + .await + .unwrap(); + + // Make a change. + universe.insert("hello".into(), BlockDef::new(AIR)).unwrap(); + + // Save it. + universe + .whence + .save(&universe, yield_progress_for_testing()) + .await + .unwrap(); + + // Check the saved result + assert_eq!( + serde_json::from_reader::<_, serde_json::Value>(fs::File::open(path).unwrap()).unwrap(), + json!({ + "type": "UniverseV1", + "members": [ + { + "name": {"Specific": "hello"}, + "member_type": "Block", + "value": {"type": "BlockV1", "primitive": {"type": "AirV1"}}, + } + ] + }) + ); +}