Skip to main content

sound_to_patch/preset/
template.rs

1//! Where the template of a preset comes from: a path on the command line,
2//! a file the adapter knows about, or a template built into the binary.
3//!
4//! [`locate_template`] applies that order. A path the user names that does
5//! not exist is an error, never a reason to fall back.
6
7use std::error::Error;
8use std::fmt;
9use std::io;
10use std::path::{Path, PathBuf};
11
12use crate::preset::format::PresetModel;
13use crate::preset::read_prefix;
14
15/// The template rules of one synth.
16pub trait TemplateSource: Send + Sync {
17    /// How a user makes a template. The missing-template error prints it.
18    fn help(&self) -> TemplateHelp;
19
20    /// Files searched after the command-line path, in order. May be empty.
21    fn candidates(&self) -> Vec<PathBuf>;
22
23    /// A template built into the binary, or `None` when the synth needs a
24    /// file saved by the app.
25    fn builtin(&self) -> Option<Vec<u8>>;
26
27    /// Errors and warnings about a template.
28    fn check(&self, model: &dyn PresetModel) -> Vec<TemplateCheck>;
29}
30
31/// The text that tells a user how to make a template.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct TemplateHelp(pub String);
34
35impl fmt::Display for TemplateHelp {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(&self.0)
38    }
39}
40
41/// How serious a [`TemplateCheck`] is.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Severity {
44    /// The template cannot be used.
45    Error,
46    /// The template works, but the output may surprise.
47    Warning,
48}
49
50/// One finding about a template.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct TemplateCheck {
53    /// How serious it is.
54    pub severity: Severity,
55    /// What was found.
56    pub text: String,
57    /// What to do about it.
58    pub fix: String,
59}
60
61impl fmt::Display for TemplateCheck {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        let level = match self.severity {
64            Severity::Error => "error",
65            Severity::Warning => "warning",
66        };
67        write!(f, "{level}: {}. {}", self.text, self.fix)
68    }
69}
70
71/// Which step of [`locate_template`] supplied the template.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum Origin {
74    /// The path on the command line.
75    CommandLine(PathBuf),
76    /// A file the adapter searches.
77    Candidate(PathBuf),
78    /// The template built into the binary.
79    Builtin,
80}
81
82impl fmt::Display for Origin {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::CommandLine(path) => write!(f, "{}", path.display()),
86            Self::Candidate(path) => write!(f, "{} (found)", path.display()),
87            Self::Builtin => write!(f, "built-in default"),
88        }
89    }
90}
91
92/// The bytes of a template and where they came from.
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct Template {
95    /// The file contents.
96    pub bytes: Vec<u8>,
97    /// Which step supplied them.
98    pub origin: Origin,
99}
100
101/// No template could be found or read.
102#[derive(Debug)]
103pub enum TemplateError {
104    /// The command-line path cannot be read.
105    Unreadable {
106        /// The path.
107        path: PathBuf,
108        /// Why.
109        cause: io::Error,
110    },
111    /// No step supplied a template.
112    Missing {
113        /// Every path that was tried, in order.
114        tried: Vec<PathBuf>,
115        /// How to make one.
116        help: TemplateHelp,
117    },
118}
119
120impl fmt::Display for TemplateError {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            Self::Unreadable { path, cause } => write!(
124                f,
125                "cannot read the template {}: {cause}. Check the path given with --template",
126                path.display()
127            ),
128            Self::Missing { tried, help } => {
129                if tried.is_empty() {
130                    return write!(f, "no template found and no path was searched. {help}");
131                }
132                write!(f, "no template found; tried:")?;
133                for path in tried {
134                    write!(f, "\n  {}", path.display())?;
135                }
136                write!(f, "\n{help}")
137            }
138        }
139    }
140}
141
142impl Error for TemplateError {
143    fn source(&self) -> Option<&(dyn Error + 'static)> {
144        match self {
145            Self::Unreadable { cause, .. } => Some(cause),
146            Self::Missing { .. } => None,
147        }
148    }
149}
150
151/// Finds the template: the command-line path, then the first existing
152/// candidate, then the built-in template.
153///
154/// # Errors
155///
156/// [`TemplateError::Unreadable`] when the command-line path cannot be read
157/// or holds more than `max` bytes, with no fallback, and
158/// [`TemplateError::Missing`] when no step supplies a template. A candidate
159/// over `max` bytes is passed over.
160pub fn locate_template(
161    source: &dyn TemplateSource,
162    cli_path: Option<&Path>,
163    max: usize,
164) -> Result<Template, TemplateError> {
165    let read = |path: &Path| {
166        let bytes = read_prefix(path, max)?;
167        if bytes.len() > max {
168            return Err(io::Error::new(
169                io::ErrorKind::InvalidData,
170                format!("the file is larger than the limit of {max} bytes"),
171            ));
172        }
173        Ok(bytes)
174    };
175    if let Some(path) = cli_path {
176        return read(path)
177            .map(|bytes| Template {
178                bytes,
179                origin: Origin::CommandLine(path.to_owned()),
180            })
181            .map_err(|cause| TemplateError::Unreadable {
182                path: path.to_owned(),
183                cause,
184            });
185    }
186    let candidates = source.candidates();
187    for path in &candidates {
188        if path.is_file()
189            && let Ok(bytes) = read(path)
190        {
191            return Ok(Template {
192                bytes,
193                origin: Origin::Candidate(path.clone()),
194            });
195        }
196    }
197    source
198        .builtin()
199        .map(|bytes| Template {
200            bytes,
201            origin: Origin::Builtin,
202        })
203        .ok_or_else(|| TemplateError::Missing {
204            tried: candidates,
205            help: source.help(),
206        })
207}