use std::path::{Path, PathBuf};
use serde_json::Value;
use super::{Sha256Digest, Template, TemplateError, TemplateLoader};
#[derive(Debug, Clone, PartialEq)]
pub struct LoaderDocument {
reference: String,
entry: String,
search_path: Vec<PathBuf>,
templates: Vec<(String, String)>,
digest: Option<Sha256Digest>,
}
impl LoaderDocument {
pub const KEYS: [&'static str; 5] =
["reference", "entry", "search_path", "templates", "digest"];
pub fn from_json(text: &str) -> Result<Self, TemplateError> {
let malformed = |message: String| TemplateError::MalformedLoader { message };
let parsed: Value = serde_json::from_str(text)
.map_err(|error| malformed(format!("it is not JSON: {error}")))?;
let Value::Object(document) = parsed else {
return Err(malformed("its top level is not a JSON object".to_owned()));
};
let text_at = |key: &str| -> Result<Option<String>, TemplateError> {
match document.get(key) {
None => Ok(None),
Some(Value::String(text)) => Ok(Some(text.clone())),
Some(_) => Err(malformed(format!("`{key}` is not a string"))),
}
};
let [
reference_key,
entry_key,
search_key,
templates_key,
digest_key,
] = Self::KEYS;
let reference = text_at(reference_key)?
.filter(|reference| !reference.is_empty())
.ok_or_else(|| malformed(format!("`{reference_key}` is missing or empty")))?;
let entry = text_at(entry_key)?
.filter(|entry| !entry.is_empty())
.ok_or_else(|| malformed(format!("`{entry_key}` is missing or empty")))?;
let digest = text_at(digest_key)?
.map(|digest| {
Sha256Digest::parse(digest)
.map_err(|problem| malformed(format!("`{digest_key}`: {problem}")))
})
.transpose()?;
let search_path = match document.get(search_key) {
None => Vec::new(),
Some(Value::Array(directories)) => directories
.iter()
.enumerate()
.map(|(index, directory)| match directory {
Value::String(directory) if Path::new(directory).is_absolute() => {
Ok(PathBuf::from(directory))
}
Value::String(directory) => Err(malformed(format!(
"`{search_key}[{index}]` is {directory:?}, which is not an absolute \
directory"
))),
_ => Err(malformed(format!(
"`{search_key}[{index}]` is not a string"
))),
})
.collect::<Result<_, _>>()?,
Some(_) => return Err(malformed(format!("`{search_key}` is not a list"))),
};
let templates = match document.get(templates_key) {
None => Vec::new(),
Some(Value::Array(pairs)) => pairs
.iter()
.enumerate()
.map(|(index, pair)| {
let field = |key: &str| match pair.get(key) {
Some(Value::String(text)) if !text.is_empty() => Ok(text.clone()),
_ => Err(malformed(format!(
"`{templates_key}[{index}]` has no non-empty string `{key}`; each \
entry is \
{{\"name\": <name>, \"source\": <text>}}"
))),
};
Ok((field("name")?, field("source")?))
})
.collect::<Result<_, TemplateError>>()?,
Some(_) => return Err(malformed(format!("`{templates_key}` is not a list"))),
};
Ok(Self {
reference,
entry,
search_path,
templates,
digest,
})
}
pub fn new(
reference: impl Into<String>,
entry: impl Into<String>,
) -> Result<Self, TemplateError> {
let (reference, entry) = (reference.into(), entry.into());
for (key, value) in [("reference", &reference), ("entry", &entry)] {
if value.is_empty() {
return Err(TemplateError::MalformedLoader {
message: format!("`{key}` is missing or empty"),
});
}
}
Ok(Self {
reference,
entry,
search_path: Vec::new(),
templates: Vec::new(),
digest: None,
})
}
pub fn with_directory(mut self, directory: impl Into<PathBuf>) -> Result<Self, TemplateError> {
let directory = directory.into();
if !directory.is_absolute() {
return Err(TemplateError::MalformedLoader {
message: format!(
"the search directory {} is not an absolute directory",
directory.display()
),
});
}
self.search_path.push(directory);
Ok(self)
}
pub fn with_template(
mut self,
name: impl Into<String>,
source: impl Into<String>,
) -> Result<Self, TemplateError> {
let (name, source) = (name.into(), source.into());
if name.is_empty() || source.is_empty() {
return Err(TemplateError::MalformedLoader {
message: "a registered template has an empty name or source".to_owned(),
});
}
self.templates.push((name, source));
Ok(self)
}
#[must_use]
pub fn with_digest(mut self, digest: Sha256Digest) -> Self {
self.digest = Some(digest);
self
}
#[must_use]
pub fn reference(&self) -> &str {
&self.reference
}
pub fn load(&self) -> Result<Template, TemplateError> {
let loader = self
.search_path
.iter()
.fold(TemplateLoader::new(), |loader, directory| {
loader.with_directory(directory)
});
let loader = self
.templates
.iter()
.fold(loader, |loader, (name, source)| {
loader.with_template(name, source)
});
let template = loader.load_name(&self.entry)?;
if let Some(stated) = &self.digest
&& stated.as_str() != template.digest()
{
return Err(TemplateError::LoaderDigest {
stated: stated.to_string(),
computed: template.digest().to_owned(),
});
}
Ok(template)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TemplateInput {
File {
path: PathBuf,
search_path: Vec<PathBuf>,
},
Loader(LoaderDocument),
}
impl TemplateInput {
pub fn load(&self) -> Result<Template, TemplateError> {
match self {
Self::File { path, search_path } => search_path
.iter()
.fold(TemplateLoader::new(), |loader, directory| {
loader.with_directory(directory)
})
.load_path(path),
Self::Loader(document) => document.load(),
}
}
#[must_use]
pub fn reference(&self) -> String {
match self {
Self::File { path, .. } => std::path::absolute(path)
.unwrap_or_else(|_| path.clone())
.display()
.to_string(),
Self::Loader(document) => document.reference().to_owned(),
}
}
}