diff --git a/.clippy.toml b/.clippy.toml index cf8538e4..efaec3a0 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,5 +1,5 @@ excessive-nesting-threshold = 3 too-many-arguments-threshold = 10 -allowed-prefixes = ["..", "GPU"] +allowed-prefixes = ["..", "GPU", "Orca"] min-ident-chars-threshold = 2 allowed-idents-below-min-chars = ["..", "k", "f", "re", "id"] diff --git a/Cargo.toml b/Cargo.toml index 33c8938c..061f19fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ indoc = "2.0.5" [lints.rust] non_ascii_idents = "deny" -# missing_docs = "warn" # temporary: add once we have docs +missing_docs = "warn" [lints.clippy] correctness = "deny" @@ -96,6 +96,5 @@ todo = { level = "warn", priority = 127 } # warn to use_debug = { level = "warn", priority = 127 } # debug print # temporary -missing_errors_doc = { level = "allow", priority = 127 } # remove once we have docs single_call_fn = { level = "allow", priority = 127 } # remove once other models are in tests_outside_test_module = { level = "allow", priority = 127 } # for now due to false-positive for integration tests: https://github.com/rust-lang/rust-clippy/pull/13038 diff --git a/README.md b/README.md index 8ca7d805..ec5da677 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,14 @@ set -e # stop early on non-zero exit cargo clippy --all-targets -- -D warnings # syntax and style tests cargo fmt --check # formatting test -cargo llvm-cov -- --nocapture # integration tests w/ coverage report +cargo llvm-cov -- --nocapture # integration tests w/ coverage summary +cargo llvm-cov --html -- --nocapture # integration tests w/ coverage report (target/llvm-cov/html/index.html) +``` + +## Docs + +```bash +cargo doc --no-deps # gen api docs (target/doc/orcapod/index.html) ``` ## Project Management diff --git a/src/error.rs b/src/error.rs index f0df8829..d9fadc8d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,6 @@ use colored::Colorize; +use glob; +use regex; use serde_yaml; use std::{ error::Error, @@ -6,124 +8,109 @@ use std::{ fmt::{Display, Formatter}, io, path::PathBuf, + result, }; +/// Shorthand for a Result that returns an `OrcaError`. +pub type Result = result::Result; -/// Wrapper around `serde_yaml::from_str` +/// Possible errors you may encounter. #[derive(Debug)] -pub struct DeserializeFailure { - pub path: PathBuf, - pub error: serde_yaml::Error, -} -impl Error for DeserializeFailure {} -impl Display for DeserializeFailure { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "{}{}{}{}", - "Failed to deserialize with error ".bright_red(), - self.error.to_string().bright_red(), - " for ".bright_red(), - self.path.to_string_lossy().bright_cyan() - ) - } +enum Kind { + /// Returned if a file is not expected to exist. + FileExists(PathBuf), + /// Returned if a file is expected to have a parent. + FileHasNoParent(PathBuf), + /// Returned if an annotation was expected to exist. + NoAnnotationFound(String, String, String), + /// Returned if a regular expression was expected to match. + NoRegexMatch, + /// Wrapper around `glob::GlobError` + GlobError(glob::GlobError), + /// Wrapper around `glob::PatternError` + GlobPaternError(glob::PatternError), + /// Wrapper around `regex::Error` + RegexError(regex::Error), + /// Wrapper around `serde_yaml::Error` + SerdeYamlError(serde_yaml::Error), + /// Wrapper around `io::Error` + IoError(io::Error), } -/// Wrapper around getting None when trying to find parent +/// A stable error API interface. #[derive(Debug)] -pub struct FileHasNoParent { - pub path: PathBuf, -} -impl Error for FileHasNoParent {} -impl Display for FileHasNoParent { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "File `{}` has no parent.", - self.path.to_string_lossy().bright_red() - ) +pub struct OrcaError(Kind); +impl Error for OrcaError {} +impl OrcaError { + /// Error constructor for `FileExists`. + pub(crate) const fn file_exists(path: PathBuf) -> Self { + Self(Kind::FileExists(path)) } -} - -/// Wrapper around `serde_yaml::to_string` -#[derive(Debug)] -pub struct SerializeFailure { - pub item_debug_string: String, - pub error: serde_yaml::Error, -} -impl Error for SerializeFailure {} -impl Display for SerializeFailure { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "{}{}{}{}", - "Failed to seralize ".bright_red(), - self.item_debug_string.bright_cyan(), - " with error ".bright_red(), - self.error.to_string().bright_red(), - ) + /// Error constructor for `FileHasNoParent`. + pub const fn file_has_no_parent(path: PathBuf) -> Self { + Self(Kind::FileHasNoParent(path)) + } + /// Error constructor for `NoAnnotationFound`. + pub(crate) const fn no_annotation_found(class: String, name: String, version: String) -> Self { + Self(Kind::NoAnnotationFound(class, name, version)) + } + /// Error constructor for `NoRegexMatch`. + pub(crate) const fn no_regex_match() -> Self { + Self(Kind::NoRegexMatch) } } - -/// Wrapper around `fs::read_to_string` and `fs::write` -#[derive(Debug)] -pub struct IOFailure { - pub path: PathBuf, - pub error: io::Error, -} -impl Error for IOFailure {} -impl Display for IOFailure { +impl Display for OrcaError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "{}{}{}{}", - "IO Error: ".bright_red(), - &self.error.to_string().bright_red(), - " at ".bright_red(), - &self.path.to_string_lossy().cyan(), - ) + match &self.0 { + Kind::FileExists(path) => { + write!( + f, + "File `{}` already exists.", + path.to_string_lossy().bright_cyan() + ) + } + Kind::FileHasNoParent(path) => { + write!( + f, + "File `{}` has no parent.", + path.to_string_lossy().bright_red() + ) + } + Kind::NoAnnotationFound(class, name, version) => { + write!(f, "No annotation found for `{name}:{version}` {class}.") + } + Kind::NoRegexMatch => { + write!(f, "No match for regex.") + } + Kind::GlobError(error) => write!(f, "{error}"), + Kind::GlobPaternError(error) => write!(f, "{error}"), + Kind::SerdeYamlError(error) => write!(f, "{error}"), + Kind::RegexError(error) => write!(f, "{error}"), + Kind::IoError(error) => write!(f, "{error}"), + } } } - -/// Raise error when file exists but unexpected -#[derive(Debug)] -pub struct FileExists { - pub path: PathBuf, +impl From for OrcaError { + fn from(error: glob::GlobError) -> Self { + Self(Kind::GlobError(error)) + } } -impl Error for FileExists {} -impl Display for FileExists { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "File `{}` already exists.", - self.path.to_string_lossy().bright_red() - ) +impl From for OrcaError { + fn from(error: glob::PatternError) -> Self { + Self(Kind::GlobPaternError(error)) } } - -/// Raise error when glob doesn't match on an annotation -#[derive(Debug)] -pub struct NoAnnotationFound { - pub class: String, - pub name: String, - pub version: String, +impl From for OrcaError { + fn from(error: serde_yaml::Error) -> Self { + Self(Kind::SerdeYamlError(error)) + } } -impl Error for NoAnnotationFound {} -impl Display for NoAnnotationFound { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "No annotation found for `{}:{}` {}.", - self.name, self.version, self.class - ) +impl From for OrcaError { + fn from(error: regex::Error) -> Self { + Self(Kind::RegexError(error)) } } - -/// Raise error when regex doesn't match -#[derive(Debug)] -pub struct NoRegexMatch; -impl Error for NoRegexMatch {} -impl Display for NoRegexMatch { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "No match for regex.") +impl From for OrcaError { + fn from(error: io::Error) -> Self { + Self(Kind::IoError(error)) } } diff --git a/src/lib.rs b/src/lib.rs index 985a7d2d..98dcb72d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,10 @@ +//! Intuitive compute pipeline orchestration with reproducibility, performance, and scalability in +//! mind. + +/// Error handling based on enumeration. pub mod error; +/// Components of the data model. pub mod model; +/// Data persistence is provided by using a store backend. pub mod store; mod util; diff --git a/src/model.rs b/src/model.rs index a2b6eb47..ea7d245c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,15 +1,22 @@ -use crate::util::{get_type_name, hash}; +use crate::{ + error::Result, + util::{get_type_name, hash}, +}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; use std::{ collections::BTreeMap, - error::Error, fs, io::{BufRead, BufReader}, path::{Path, PathBuf}, + result, }; - -pub fn to_yaml(instance: &T) -> Result> { +/// Converts a model instance into a consistent yaml. +/// +/// # Errors +/// +/// Will return `Err` if there is an issue converting an `instance` into YAML (w/o annotation). +pub fn to_yaml(instance: &T) -> Result { let mapping: BTreeMap = serde_yaml::from_str(&serde_yaml::to_string(instance)?)?; // sort let mut yaml = serde_yaml::to_string( &mapping @@ -21,17 +28,22 @@ pub fn to_yaml(instance: &T) -> Result> { Ok(yaml) } - +/// Instantiates a model from from yaml content and its unique hash. +/// +/// # Errors +/// +/// Will return `Err` if there is an issue converting YAML files for spec+annotation into a model +/// instance. pub fn from_yaml( annotation_file: &Path, spec_file: &Path, hash: &str, -) -> Result> { +) -> Result { let annotation: Mapping = serde_yaml::from_str(&fs::read_to_string(annotation_file)?)?; let spec_yaml = BufReader::new(fs::File::open(spec_file)?) .lines() .skip(1) - .collect::, _>>()? + .collect::, _>>()? .join("\n"); let mut spec_mapping: BTreeMap = serde_yaml::from_str(&spec_yaml)?; @@ -44,9 +56,12 @@ pub fn from_yaml( // --- core model structs --- +/// A reusable, containerized computational unit. #[derive(Serialize, Deserialize, Debug)] pub struct Pod { + /// Metadata that doesn't affect reproducibility. pub annotation: Annotation, + /// Unique id based on reproducibility. pub hash: String, source_commit_url: String, image: String, @@ -60,6 +75,11 @@ pub struct Pod { } impl Pod { + /// Construct a new pod instance. + /// + /// # Errors + /// + /// Will return `Err` if there is an issue initializing a `Pod` instance. pub fn new( annotation: Annotation, source_commit_url: String, @@ -71,7 +91,7 @@ impl Pod { recommended_cpus: f32, recommended_memory: u64, required_gpu: Option, - ) -> Result> { + ) -> Result { let pod_no_hash = Self { annotation, hash: String::new(), @@ -94,28 +114,40 @@ impl Pod { // --- util types --- +/// Standard metadata structure for all model instances. #[derive(Serialize, Deserialize, Debug)] pub struct Annotation { + /// A unique name. pub name: String, + /// A unique semantic version. pub version: String, + /// A long form description. pub description: String, } - +/// Specification for GPU requirements in computation. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GPURequirement { + /// GPU model specification. pub model: GPUModel, + /// Manufacturer recommended memory. pub recommended_memory: u64, + /// Number of GPU cards required. pub count: u16, } - +/// GPU model specification. #[derive(Serialize, Deserialize, Debug, Clone)] pub enum GPUModel { - NVIDIA(String), // String will be the specific model of the gpu + /// NVIDIA-manufactured card where `String` is the specific model e.g. ??? + NVIDIA(String), + /// AMD-manufactured card where `String` is the specific model e.g. ??? AMD(String), } - +/// Streams are named and represent an abstration for the file(s) that represent some particular +/// data. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StreamInfo { + /// Path to stream file. pub path: PathBuf, + /// Naming pattern for the stream. pub match_pattern: String, } diff --git a/src/store/filestore.rs b/src/store/filestore.rs index 0ad1f641..a9110fa1 100644 --- a/src/store/filestore.rs +++ b/src/store/filestore.rs @@ -1,28 +1,25 @@ use crate::{ - error::{FileExists, FileHasNoParent, NoAnnotationFound, NoRegexMatch}, + error::{OrcaError, Result}, model::{from_yaml, to_yaml, Pod}, store::Store, }; use colored::Colorize; -use glob::{GlobError, Paths}; use regex::Regex; use std::{ collections::BTreeMap, - error::Error, fs, - iter::Map, path::{Path, PathBuf}, }; - +/// Support for a storage backend on a local filesystem directory. #[derive(Debug)] pub struct LocalFileStore { + /// A local path to a directory where store will be located. pub directory: PathBuf, } impl Store for LocalFileStore { - fn save_pod(&self, pod: &Pod) -> Result<(), Box> { + fn save_pod(&self, pod: &Pod) -> Result<()> { let class = "pod"; - // Save the annotation file and throw and error if exist Self::save_file( &self.make_annotation_path( @@ -34,7 +31,6 @@ impl Store for LocalFileStore { &serde_yaml::to_string(&pod.annotation)?, true, )?; - // Save the pod and skip if it already exist, for the case of many annotation to a single pod Self::save_file( &self.make_spec_path(class, &pod.hash), @@ -45,16 +41,14 @@ impl Store for LocalFileStore { Ok(()) } - fn load_pod(&self, name: &str, version: &str) -> Result> { + fn load_pod(&self, name: &str, version: &str) -> Result { let class = "pod".to_owned(); let (_, (hash, _)) = Self::parse_annotation_path(&self.make_annotation_path("pod", "*", name, version))? .next() - .ok_or_else(|| NoAnnotationFound { - class, - name: name.to_owned(), - version: version.to_owned(), + .ok_or_else(|| { + OrcaError::no_annotation_found(class, name.to_owned(), version.to_owned()) })??; from_yaml::( @@ -64,10 +58,10 @@ impl Store for LocalFileStore { ) } - fn list_pod(&self) -> Result>, Box> { + fn list_pod(&self) -> Result>> { let (names, (hashes, versions)) = Self::parse_annotation_path(&self.make_annotation_path("pod", "*", "*", "*"))? - .collect::, (Vec<_>, Vec<_>)), _>>()?; + .collect::, (Vec<_>, Vec<_>))>>()?; Ok(BTreeMap::from([ (String::from("name"), names), @@ -76,24 +70,22 @@ impl Store for LocalFileStore { ])) } - fn delete_pod(&self, name: &str, version: &str) -> Result<(), Box> { + fn delete_pod(&self, name: &str, version: &str) -> Result<()> { // assumes propagate = false let class = "pod".to_owned(); let versions = self.get_pod_version_map(name)?; - let hash = versions.get(version).ok_or_else(|| NoAnnotationFound { - class, - name: name.to_owned(), - version: version.to_owned(), + let hash = versions.get(version).ok_or_else(|| { + OrcaError::no_annotation_found(class, name.to_owned(), version.to_owned()) })?; let annotation_file = self.make_annotation_path("pod", hash, name, version); - let annotation_dir = annotation_file.parent().ok_or_else(|| FileHasNoParent { - path: annotation_file.clone(), - })?; + let annotation_dir = annotation_file + .parent() + .ok_or_else(|| OrcaError::file_has_no_parent(annotation_file.clone()))?; let spec_file = self.make_spec_path("pod", hash); - let spec_dir = spec_file.parent().ok_or_else(|| FileHasNoParent { - path: spec_file.clone(), - })?; + let spec_dir = spec_file + .parent() + .ok_or_else(|| OrcaError::file_has_no_parent(spec_file.clone()))?; fs::remove_file(&annotation_file)?; if !versions @@ -114,12 +106,13 @@ impl Store for LocalFileStore { } impl LocalFileStore { + /// Construct a local file store instance. pub fn new(directory: impl Into) -> Self { Self { directory: directory.into(), } } - + /// Path where annotation file is located. pub fn make_annotation_path( &self, class: &str, @@ -137,7 +130,7 @@ impl LocalFileStore { version, )) } - + /// Path where specification file is located. pub fn make_spec_path(&self, class: &str, hash: &str) -> PathBuf { PathBuf::from(format!( "{}/{}/{}/{}", @@ -150,13 +143,7 @@ impl LocalFileStore { fn parse_annotation_path( path: &Path, - ) -> Result< - Map< - Paths, - impl FnMut(Result) -> Result<(String, (String, String)), Box>, - >, - Box, - > { + ) -> Result>> { let paths = glob::glob(&path.to_string_lossy())?.map(|filepath| { let re = Regex::new( r"(?x) @@ -170,36 +157,35 @@ impl LocalFileStore { $", )?; let filepath_string = String::from(filepath?.to_string_lossy()); - let group = re.captures(&filepath_string).ok_or(NoRegexMatch {})?; + let group = re + .captures(&filepath_string) + .ok_or_else(OrcaError::no_regex_match)?; Ok(( group["name"].to_string(), (group["hash"].to_string(), group["version"].to_string()), )) }); - Ok(paths) } - fn get_pod_version_map(&self, name: &str) -> Result, Box> { + fn get_pod_version_map(&self, name: &str) -> Result> { Self::parse_annotation_path(&self.make_annotation_path("pod", "*", name, "*"))? - .map(|metadata| -> Result<(String, String), Box> { + .map(|metadata| -> Result<(String, String)> { let resolved_metadata = metadata?; let hash = resolved_metadata.1 .0; let version = resolved_metadata.1 .1; Ok((version, hash)) }) - .collect::, _>>() + .collect::>>() } - fn save_file(file: &Path, content: &str, fail_if_exists: bool) -> Result<(), Box> { - fs::create_dir_all(file.parent().ok_or_else(|| FileHasNoParent { - path: file.to_path_buf(), - })?)?; + fn save_file(file: &Path, content: &str, fail_if_exists: bool) -> Result<()> { + if let Some(parent) = file.parent() { + fs::create_dir_all(parent)?; + } let file_exists = file.exists(); if file_exists && fail_if_exists { - return Err(Box::new(FileExists { - path: file.to_path_buf(), - })); + return Err(OrcaError::file_exists(file.to_path_buf())); } else if file_exists { println!( "Skip saving `{}` since it is already stored.", diff --git a/src/store/mod.rs b/src/store/mod.rs index b7ee2421..2e3e3d30 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,11 +1,34 @@ -use crate::model::Pod; -use std::{collections::BTreeMap, error::Error}; +use crate::{error::Result, model::Pod}; +use std::collections::BTreeMap; +/// Standard behavior of any store backend supported. pub trait Store { - fn save_pod(&self, pod: &Pod) -> Result<(), Box>; - fn load_pod(&self, name: &str, version: &str) -> Result>; - fn list_pod(&self) -> Result>, Box>; - fn delete_pod(&self, name: &str, version: &str) -> Result<(), Box>; + /// How a pod is stored. + /// + /// # Errors + /// + /// Will return `Err` if there is an issue storing `pod`. + fn save_pod(&self, pod: &Pod) -> Result<()>; + /// How to load a stored pod into a model instance. + /// + /// # Errors + /// + /// Will return `Err` if there is an issue loading a pod from the store using `name` and + /// `version`. + fn load_pod(&self, name: &str, version: &str) -> Result; + /// How to query stored pods. + /// + /// # Errors + /// + /// Will return `Err` if there is an issue querying metadata from existing pods in the store. + fn list_pod(&self) -> Result>>; + /// How to delete a stored pod (does not propagate). + /// + /// # Errors + /// + /// Will return `Err` if there is an issue deleting a pod from the store using `name` and + /// `version`. + fn delete_pod(&self, name: &str, version: &str) -> Result<()>; } - +/// Store implementation on a local filesystem. pub mod filestore; diff --git a/src/util.rs b/src/util.rs index 42f5252e..9e71191a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,7 +3,7 @@ use std::any::type_name; #[expect( clippy::unwrap_used, - reason = "`None` impossible since `type_name` always returns `&str`." + reason = "`last()` cannot return `None` since `type_name` always returns `&str`." )] pub fn get_type_name() -> String { type_name::() diff --git a/tests/fixture/mod.rs b/tests/fixture/mod.rs index 8fd0e4ad..d9015a7a 100644 --- a/tests/fixture/mod.rs +++ b/tests/fixture/mod.rs @@ -1,11 +1,17 @@ +#![expect( + clippy::missing_errors_doc, + reason = "Integration tests won't be included in documentation." +)] + use orcapod::{ + error::Result, model::{Annotation, Pod, StreamInfo}, store::{filestore::LocalFileStore, Store}, }; -use std::{collections::BTreeMap, error::Error, fs, ops::Deref, path::PathBuf}; +use std::{collections::BTreeMap, fs, ops::Deref, path::PathBuf}; use tempfile::tempdir; -pub fn pod_style() -> Result> { +pub fn pod_style() -> Result { Pod::new( Annotation { name: "style-transfer".to_owned(), @@ -50,11 +56,7 @@ pub struct TestLocalStore { store: LocalFileStore, } -#[expect( - clippy::expect_used, - reason = "Required since can't modify drop signature." -)] -pub fn store_test(store_directory: Option<&str>) -> Result> { +pub fn store_test(store_directory: Option<&str>) -> Result { impl Deref for TestLocalStore { type Target = LocalFileStore; fn deref(&self) -> &Self::Target { @@ -62,6 +64,10 @@ pub fn store_test(store_directory: Option<&str>) -> Result { pod: Pod, } -#[expect( - clippy::expect_used, - reason = "Required since can't modify drop signature." -)] -pub fn add_pod_storage( - pod: Pod, - store: &TestLocalStore, -) -> Result> { +pub fn add_pod_storage(pod: Pod, store: &TestLocalStore) -> Result { impl<'base> Deref for TestLocallyStoredPod<'base> { type Target = Pod; fn deref(&self) -> &Self::Target { @@ -93,6 +92,10 @@ pub fn add_pod_storage( } } impl<'base> Drop for TestLocallyStoredPod<'base> { + #[expect( + clippy::expect_used, + reason = "Required since can't modify drop signature." + )] fn drop(&mut self) { self.store .delete_pod(&self.pod.annotation.name, &self.pod.annotation.version) diff --git a/tests/model.rs b/tests/model.rs index ca467e2d..a1749a99 100644 --- a/tests/model.rs +++ b/tests/model.rs @@ -1,13 +1,15 @@ #![expect(clippy::panic_in_result_fn, reason = "Panics OK in tests.")] -use std::error::Error; pub mod fixture; use fixture::pod_style; use indoc::indoc; -use orcapod::model::{to_yaml, Pod}; +use orcapod::{ + error::Result, + model::{to_yaml, Pod}, +}; #[test] -fn verify_hash() -> Result<(), Box> { +fn verify_hash() -> Result<()> { assert_eq!( pod_style()?.hash, "13D69656D396C272588DD875B2802FAEE1A56BD985E3C43C7DB276A373BC9DDB" @@ -16,7 +18,7 @@ fn verify_hash() -> Result<(), Box> { } #[test] -fn verify_pod_to_yaml() -> Result<(), Box> { +fn verify_pod_to_yaml() -> Result<()> { assert_eq!( to_yaml::(&pod_style()?)?, indoc! {" diff --git a/tests/store.rs b/tests/store.rs index 07ea0c4e..e212c3a0 100644 --- a/tests/store.rs +++ b/tests/store.rs @@ -3,29 +3,25 @@ pub mod fixture; use fixture::{add_pod_storage, pod_style, store_test}; use orcapod::{ - error::FileHasNoParent, + error::{OrcaError, Result}, model::{to_yaml, Pod}, }; -use std::{error::Error, fs, path::Path}; +use std::{fs, path::Path}; use tempfile::tempdir; -fn is_dir_two_levels_up_empty(file: &Path) -> Result> { +fn is_dir_two_levels_up_empty(file: &Path) -> Result { Ok(file .parent() - .ok_or_else(|| FileHasNoParent { - path: file.to_path_buf(), - })? + .ok_or_else(|| OrcaError::file_has_no_parent(file.to_path_buf()))? .parent() - .ok_or_else(|| FileHasNoParent { - path: file.to_path_buf(), - })? + .ok_or_else(|| OrcaError::file_has_no_parent(file.to_path_buf()))? .read_dir()? .next() .is_none()) } #[test] -fn verify_pod_save_and_delete() -> Result<(), Box> { +fn verify_pod_save_and_delete() -> Result<()> { let store_directory = String::from(tempdir()?.path().to_string_lossy()); { let pod_style = pod_style()?;