architect_api/utils/
maybe_file.rs

1#[cfg(feature = "tokio")]
2use anyhow::bail;
3use anyhow::Result;
4use serde::{de::DeserializeOwned, Deserialize, Serialize};
5use std::{fmt::Display, path::PathBuf};
6
7/// A type that is either a file containing the value or the value itself.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum MaybeFile<T> {
10    Value(T),
11    File(PathBuf),
12}
13
14impl<T: Display> Display for MaybeFile<T> {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match &*self {
17            MaybeFile::Value(v) => write!(f, "{}", v),
18            MaybeFile::File(p) => write!(f, "{}", p.display()),
19        }
20    }
21}
22
23impl<T: Clone + DeserializeOwned> MaybeFile<T> {
24    #[cfg(feature = "tokio")]
25    pub async fn load(&self) -> Result<T> {
26        match self {
27            MaybeFile::Value(v) => Ok(v.clone()),
28            MaybeFile::File(p) => {
29                let contents = tokio::fs::read_to_string(p).await?;
30                let t = match p.extension().and_then(std::ffi::OsStr::to_str) {
31                    Some("json") => serde_json::from_str(&contents)?,
32                    Some("yml") | Some("yaml") => serde_yaml::from_str(&contents)?,
33                    Some(other) => bail!("unknown file extension: {}", other),
34                    None => bail!("no file extension"),
35                };
36                Ok(t)
37            }
38        }
39    }
40}
41
42impl<'de, T: DeserializeOwned> Deserialize<'de> for MaybeFile<T> {
43    fn deserialize<D>(de: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        #[derive(Serialize, Deserialize)]
48        #[serde(untagged)]
49        enum Format<T> {
50            Value(T),
51            File(String),
52        }
53        match Format::<T>::deserialize(de)? {
54            Format::Value(t) => Ok(MaybeFile::Value(t)),
55            Format::File(s) => Ok(MaybeFile::File(PathBuf::from(s))),
56        }
57    }
58}