1use std::{ffi::OsStr, fs, io, path::PathBuf};
2
3use crate::{error::ErrorInner, Error, Partial};
4
5
6pub struct File {
12 path: PathBuf,
13 format: FileFormat,
14 required: bool,
15}
16
17impl File {
18 pub fn new(path: impl Into<PathBuf>) -> Result<Self, Error> {
22 let path = path.into();
23 let ext = path
24 .extension()
25 .ok_or_else(|| ErrorInner::MissingFileExtension { path: path.clone() })?;
26 let format = FileFormat::from_extension(ext)
27 .ok_or_else(|| ErrorInner::UnsupportedFileFormat { path: path.clone() })?;
28
29 Ok(Self::with_format(path, format))
30 }
31
32 pub fn with_format(path: impl Into<PathBuf>, format: FileFormat) -> Self {
34 Self {
35 path: path.into(),
36 format,
37 required: false,
38 }
39 }
40
41 pub fn required(mut self) -> Self {
45 self.required = true;
46 self
47 }
48
49 pub fn load<P: Partial>(&self) -> Result<P, Error> {
51 let file_content = match fs::read(&self.path) {
54 Ok(v) => v,
55 Err(e) if e.kind() == io::ErrorKind::NotFound => {
56 if self.required {
57 return Err(ErrorInner::MissingRequiredFile { path: self.path.clone() }.into());
58 } else {
59 return Ok(P::empty());
60 }
61 }
62 Err(e) => {
63 return Err(ErrorInner::Io {
64 path: Some(self.path.clone()),
65 err: e,
66 }.into());
67 }
68 };
69
70 let error = |err| {
72 Error::from(ErrorInner::Deserialization {
73 err,
74 source: Some(format!("file '{}'", self.path.display())),
75 })
76 };
77
78 match self.format {
79 #[cfg(feature = "toml")]
80 FileFormat::Toml => {
81 let s = std::str::from_utf8(&file_content).map_err(|e| error(Box::new(e)))?;
82 toml::from_str(s).map_err(|e| error(Box::new(e)))
83 }
84
85 #[cfg(feature = "yaml")]
86 FileFormat::Yaml => serde_yaml::from_slice(&file_content)
87 .map_err(|e| error(Box::new(e))),
88
89 #[cfg(feature = "json5")]
90 FileFormat::Json5 => {
91 let s = std::str::from_utf8(&file_content).map_err(|e| error(Box::new(e)))?;
92 json5::from_str(s).map_err(|e| error(Box::new(e)))
93 }
94 }
95 }
96}
97
98pub enum FileFormat {
102 #[cfg(feature = "toml")]
103 Toml,
104 #[cfg(feature = "yaml")]
105 Yaml,
106 #[cfg(feature = "json5")]
107 Json5,
108}
109
110impl FileFormat {
111 pub fn from_extension(ext: impl AsRef<OsStr>) -> Option<Self> {
114 match ext.as_ref().to_str()? {
115 #[cfg(feature = "toml")]
116 "toml" => Some(Self::Toml),
117
118 #[cfg(feature = "yaml")]
119 "yaml" | "yml" => Some(Self::Yaml),
120
121 #[cfg(feature = "json5")]
122 "json5" | "json" => Some(Self::Json5),
123
124 _ => None,
125 }
126 }
127}