use std::{
fmt::{Debug, Display},
path::{Path, PathBuf},
};
use datasize::DataSize;
#[cfg(test)]
use once_cell::sync::Lazy;
use openssl::{
pkey::{PKey, Private},
x509::X509,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use casper_types::SecretKey;
use super::{read_file, ReadFileError};
use crate::{crypto, crypto::AsymmetricKeyExt, tls};
#[cfg(test)]
pub static RESOURCES_PATH: Lazy<PathBuf> =
Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../resources"));
#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum External<T> {
Path(PathBuf),
#[serde(skip)]
Loaded(T),
#[serde(skip)]
Missing,
}
impl<T> External<T> {
pub fn value(value: T) -> Self {
External::Loaded(value)
}
pub fn path<P: AsRef<Path>>(path: P) -> Self {
External::Path(path.as_ref().to_owned())
}
}
impl<T> External<T>
where
T: Loadable,
{
pub fn load<P: AsRef<Path>>(self, root: P) -> Result<T, LoadError<T::Error>> {
match self {
External::Path(path) => {
let full_path = if path.is_relative() {
root.as_ref().join(&path)
} else {
path
};
T::from_file(&full_path).map_err(move |error| LoadError::Failed {
error,
path: full_path.canonicalize().unwrap_or(full_path),
})
}
External::Loaded(value) => Ok(value),
External::Missing => Err(LoadError::Missing),
}
}
}
impl<T> External<T>
where
T: Loadable + Default,
{
pub fn with_default(self) -> Self {
match self {
External::Missing => External::Loaded(Default::default()),
_ => self,
}
}
}
pub trait Loadable: Sized {
type Error: Debug + Display;
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>;
#[cfg(test)]
fn from_resources<P: AsRef<Path>>(rel_path: P) -> Self {
Self::from_file(RESOURCES_PATH.join(rel_path)).expect("could not load resources from local")
}
}
impl<T> Default for External<T> {
fn default() -> Self {
External::Missing
}
}
fn display_res_path<E>(result: &Result<PathBuf, E>) -> String {
result
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| String::new())
}
#[derive(Debug, Error)]
pub enum LoadError<E: Debug + Display> {
#[error("could not load from {}: {error}", display_res_path(&.path.canonicalize()))]
Failed {
path: PathBuf,
error: E,
},
#[error("value is missing (default requested)")]
Missing,
}
impl Loadable for X509 {
type Error = anyhow::Error;
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
tls::load_cert(path)
}
}
impl Loadable for PKey<Private> {
type Error = anyhow::Error;
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
tls::load_private_key(path)
}
}
impl Loadable for SecretKey {
type Error = crypto::Error;
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
AsymmetricKeyExt::from_file(path)
}
}
impl Loadable for Vec<u8> {
type Error = ReadFileError;
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
read_file(path)
}
}
#[cfg(test)]
mod tests {
use super::External;
#[test]
fn test_to_string() {
let val: External<()> = External::Path("foo/bar.toml".into());
assert_eq!(
"\"foo/bar.toml\"",
serde_json::to_string(&val).expect("serialization error")
);
}
#[test]
fn test_load_from_string() {
let input = "\"foo/bar.toml\"";
let val: External<()> = serde_json::from_str(input).expect("deserialization failed");
assert_eq!(External::Path("foo/bar.toml".into()), val);
}
}