ap_manual 0.2.0

A rust package to cannonically interact with manual AP worlds
Documentation
use std::{error::Error, fmt::Display, fs::File, io::{self, Read, Seek, Write}, path::{Path, PathBuf}};

use zip::{read::root_dir_common_filter, write::SimpleFileOptions};

use crate::{category::{self}, game, item, location, meta, options, region::Regions, APWorld};
///Cannonical AP read or write error
#[derive(Debug)]
pub enum APIOError{
    FileError(io::Error),
    ZipError(zip::result::ZipError),
    SerdeError(serde_json::Error),
}
impl Display for APIOError{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self{
            Self::FileError(v)=>v.fmt(f),
            Self::ZipError(v)=>v.fmt(f),
            Self::SerdeError(v)=>v.fmt(f)
        } 
    }
}
impl Error for APIOError{
}
impl From<io::Error> for APIOError{
    fn from(value: io::Error) -> Self {
        Self::FileError(value)
    }
}
impl From<zip::result::ZipError> for APIOError{
    fn from(value: zip::result::ZipError) -> Self {
        Self::ZipError(value)
    }
}
impl From<serde_json::Error> for APIOError{
    fn from(value: serde_json::Error) -> Self {
        Self::SerdeError(value)
    }
}
/// Attempts to decode a manual ap world from a reader
/// # Errors
/// Reading fails if:
/// the reader is not a zip(.apworld files are zips)
/// the zip structure is not as expected
/// the json config files are not present or invalid 
#[must_use]
pub fn read_apworld<R: Read+Seek>(reader: R)->Result<APWorld,APIOError>{
    let mut archive = zip::ZipArchive::new(reader)?;
    let r=archive.root_dir(root_dir_common_filter)?.ok_or(APIOError::ZipError(zip::result::ZipError::FileNotFound))?;
    let r=r.join("data");
    let categories = archive.by_path(r.join("categories.json"))?;
    let categories: category::Categories=serde_json::from_reader(categories)?;
    let game = archive.by_path(r.join("game.json"))?;
    let game: game::Game=serde_json::from_reader(game)?;
    let items: Vec<item::Item> = serde_json::from_reader(archive.by_path(r.join("items.json"))?)?;
    let locations: Vec<location::Location> = serde_json::from_reader(archive.by_path(r.join("locations.json"))?)?;
    let meta: meta::Meta = serde_json::from_reader(archive.by_path(r.join("meta.json"))?)?;
    let options: options::Options = serde_json::from_reader(archive.by_path(r.join("options.json"))?)?;
    let regions: Regions = serde_json::from_reader(archive.by_path(r.join("regions.json"))?)?;
    Ok(crate::APWorld{
        category: categories,
        game, items, locations, meta, options, regions
    })
}
/// Attept to read the ap world directly from a file
/// See {read_apworld}
#[must_use]
pub fn read_apworld_from_file(path: &Path)->Result<APWorld,APIOError>{
    let f= File::open(path)?;
    read_apworld(f)
}
/// Attemps to write the ap world to the designated writer
/// stem is the top level root folder to place the items in
/// # Errors
/// Writing fails if adding any of the components to the zip fails or the writer fails
#[must_use]
pub fn write_apworld<W: Write+Seek>(writer: W, stem: &str,world:&APWorld,method: zip::CompressionMethod)->Result<(),APIOError>{
    let mut zip = zip::ZipWriter::new(writer);
    let opts = SimpleFileOptions::default().compression_method(method).unix_permissions(0o755);
    let tl =Path::new(stem);
    //dynamic portion (data)
    zip.add_directory_from_path(tl, opts)?;
    let r: PathBuf=tl.join(Path::new("data"));
    zip.add_directory_from_path(&r, opts)?;
    zip.start_file_from_path(r.join("categories.json"),opts )?;
    serde_json::to_writer(&mut zip,&world.category)?;
    zip.start_file_from_path(r.join("game.json"), opts)?;
    serde_json::to_writer(&mut zip, &world.game)?;
    zip.start_file_from_path(r.join("items.json"), opts)?;
    serde_json::to_writer(&mut zip, &world.items)?;
    zip.start_file_from_path(r.join("locations.json"), opts)?;
    serde_json::to_writer(&mut zip, &world.locations)?;
    zip.start_file_from_path(r.join("meta.json"), opts)?;
    serde_json::to_writer(&mut zip, &world.meta)?;
    zip.start_file_from_path(r.join("options.json"),opts)?;
    serde_json::to_writer(&mut zip, &world.options)?;
    zip.start_file_from_path(r.join("regions.json"), opts)?;
    serde_json::to_writer(&mut zip, &world.regions)?;
    //surely skipping docs does nothing
    //nex write hooks
    let r: PathBuf=tl.join(Path::new("hooks"));
    zip.add_directory_from_path(&r, opts)?;
    zip.start_file_from_path(r.join("__init__.py"),opts)?;
    zip.write_all(include_bytes!("static_files/hooks/__init__.py"))?;//probably not strictly required
    zip.start_file_from_path(r.join("Data.py"), opts)?;
    zip.write_all(include_bytes!("static_files/hooks/Data.py"))?;
    zip.start_file_from_path(r.join("Helpers.py"), opts)?;
    zip.write_all(include_bytes!("static_files/hooks/Helpers.py"))?;
    zip.start_file_from_path(r.join("Options.py"), opts)?;
    zip.write_all(include_bytes!("static_files/hooks/Options.py"))?;
    zip.start_file_from_path(r.join("Rules.py"), opts)?;
    zip.write_all(include_bytes!("static_files/hooks/Rules.py"))?;
    zip.start_file_from_path(r.join("World.py"), opts)?;
    zip.write_all(include_bytes!("static_files/hooks/World.py"))?;
    //main dir files
    zip.start_file_from_path(tl.join("__init__.py"),opts)?;
    zip.write_all(include_bytes!("static_files/__init__.py"))?;
    zip.start_file_from_path(tl.join("Data.py"),opts)?;
    zip.write_all(include_bytes!("static_files/Data.py"))?;
    zip.start_file_from_path(tl.join("DataValidation.py"), opts)?;
    zip.write_all(include_bytes!("static_files/DataValidation.py"))?;
    zip.start_file_from_path(tl.join("Game.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Game.py"))?;
    zip.start_file_from_path(tl.join("Helpers.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Helpers.py"))?;
    zip.start_file_from_path(tl.join("Items.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Items.py"))?;
    zip.start_file_from_path(tl.join("Locations.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Locations.py"))?;
    zip.start_file_from_path(tl.join("manual_test.py"), opts)?;
    zip.write_all(include_bytes!("static_files/manual_test.py"))?;
    zip.start_file_from_path(tl.join("ManualClient.py"), opts)?;
    zip.write_all(include_bytes!("static_files/ManualClient.py"))?;
    zip.start_file_from_path(tl.join("Meta.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Meta.py"))?;
    zip.start_file_from_path(tl.join("Options.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Options.py"))?;
    zip.start_file_from_path(tl.join("Regions.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Regions.py"))?;
    zip.start_file_from_path(tl.join("Rules.py"), opts)?;
    zip.write_all(include_bytes!("static_files/Rules.py"))?;
    zip.finish()?;
    Ok(())
}
/// Attempt to write an apworld directly to file
/// See [write_apworld]
#[must_use]
pub fn write_apworld_to_file(path: &Path,world:&APWorld,method: zip::CompressionMethod)->Result<(),APIOError>{
    let f=File::open(path)?;
    let stem = path.file_stem()
        .ok_or(APIOError::FileError(io::Error::new(io::ErrorKind::InvalidFilename, "File has no stem")))?;
    let stem = stem.to_str().ok_or(io::Error::new(io::ErrorKind::InvalidFilename,"Non unicode filename"))?;
    write_apworld(f, stem, world, method)
}