orphism 0.3.0

a library for interacting with a certain format sometimes used for VTuber model assets
Documentation
use crate::model::Model;
use crate::{Error, Result};
use cap_std::fs::{Dir, File};
use std::io::Read;
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub struct Runtime {
  root: Dir,
  model: PathBuf,
}

impl Runtime {
  pub fn new_from_model_path(model: PathBuf) -> Result<Self> {
    log::trace!("new_from_model_path({model:?}");

    if !model.is_file() {
      return Err(Error::ModelPathIsNotAFile(model));
    }

    match model.parent() {
      None => Err(Error::ModelHasNoParentDirectory(model)),
      Some(parent) => match model.file_name() {
        None => Err(Error::ModelPathMayNotEndWithTrailingDots(model)),
        Some(file_name) => {
          let root = Dir::from_std_file(std::fs::File::open(parent)?);
          let model = PathBuf::from(file_name);
          Ok(Self { model, root })
        }
      },
    }
  }

  pub fn new_from_runtime_path(root: PathBuf) -> Result<Self> {
    log::trace!("new_from_runtime_path({root:?}");

    if !root.is_dir() {
      return Err(Error::RuntimePathIsNotADirectory(root));
    }

    let mut models = Vec::new();

    for entry in root.read_dir()? {
      let file_name = entry?.file_name();
      if file_name.to_string_lossy().ends_with(".model3.json") {
        let model = PathBuf::from(file_name);
        models.push(model);
      }
    }

    match models.len() {
      0 => Err(Error::RuntimePathContainsNoModels(root)),
      1 => {
        let model = root.join(models.swap_remove(0));
        Self::new_from_model_path(model)
      }
      _ => Err(Error::RuntimePathContainsMultipleModels(root)),
    }
  }

  pub fn open_dir<P>(&self, path: P) -> Result<Dir>
  where
    P: AsRef<Path>,
  {
    Ok(self.root.open_dir(path)?)
  }

  pub fn open_file<P>(&self, path: P) -> Result<File>
  where
    P: AsRef<Path>,
  {
    Ok(self.root.open(path)?)
  }

  pub fn read_bytes<P>(&self, path: P) -> Result<Vec<u8>>
  where
    P: AsRef<Path>,
  {
    let mut file = self.open_file(path)?;
    let capacity = file.metadata()?.len().try_into().unwrap_or(0);
    let mut buf = Vec::with_capacity(capacity);
    file.read_to_end(&mut buf)?;
    buf.shrink_to_fit();
    Ok(buf)
  }

  pub fn read_json<P, T>(&self, path: P) -> Result<T>
  where
    P: AsRef<Path>,
    T: serde::de::DeserializeOwned,
  {
    Ok(serde_json::from_slice(&self.read_bytes(path)?)?)
  }

  pub fn load_model(&self) -> Result<Model> {
    self.read_json(self.model.as_path()).and_then(|model| Model::load(self, model))
  }
}