use schemars::JsonSchema;
use serde::Deserialize;
use super::super::files::Mode;
use super::super::mistake::Mistake;
use super::super::problem::Code;
use super::words::{AbsPath, GroupName, HttpsUrl, ProjectPath, Sha256, UserName};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Copy {
pub from: ProjectPath,
pub to: AbsPath,
#[serde(default)]
pub owner: Option<UserName>,
#[serde(default)]
pub mode: Option<Mode>,
}
impl Copy {
#[must_use]
pub fn is_folder(&self) -> bool {
self.from.as_str().ends_with('/')
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(try_from = "TemplateInput")]
pub struct Template {
pub from: ProjectPath,
pub to: AbsPath,
pub owner: Option<UserName>,
pub group: Option<GroupName>,
pub mode: Option<Mode>,
}
schema_as!(Template, TemplateInput);
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct TemplateInput {
from: ProjectPath,
to: AbsPath,
#[serde(default)]
owner: Option<UserName>,
#[serde(default)]
group: Option<GroupName>,
#[serde(default)]
mode: Option<Mode>,
}
impl TryFrom<TemplateInput> for Template {
type Error = Mistake;
fn try_from(t: TemplateInput) -> Result<Template, Mistake> {
if t.from.as_str().ends_with('/') {
return Err(Mistake::new(Code::InvalidValue, format!("`{}`: a template is one file", t.from))
.at("from")
.fix("a `copy` step copies a folder"));
}
Ok(Template {
from: t.from,
to: t.to,
owner: t.owner,
group: t.group,
mode: t.mode,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(try_from = "FileInput")]
pub struct FileStep {
pub path: AbsPath,
pub state: FileState,
pub owner: Option<UserName>,
pub group: Option<GroupName>,
pub mode: Option<Mode>,
}
schema_as!(FileStep, FileInput);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FileState {
Directory,
Absent,
Link(AbsPath),
Download {
url: HttpsUrl,
sha256: Sha256,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
enum StateWord {
Directory,
Absent,
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct FileInput {
path: AbsPath,
#[serde(default)]
state: Option<StateWord>,
#[serde(default)]
link: Option<AbsPath>,
#[serde(default)]
url: Option<HttpsUrl>,
#[serde(default)]
sha256: Option<Sha256>,
#[serde(default)]
owner: Option<UserName>,
#[serde(default)]
group: Option<GroupName>,
#[serde(default)]
mode: Option<Mode>,
}
impl TryFrom<FileInput> for FileStep {
type Error = Mistake;
fn try_from(f: FileInput) -> Result<FileStep, Mistake> {
let state = state(f.state, f.link, f.url, f.sha256)?;
let owned = f.owner.is_some() || f.group.is_some() || f.mode.is_some();
match state {
FileState::Absent if owned => {
return Err(Mistake::new(
Code::Conflict,
"`state: absent` removes the path, so it has no owner, group or mode",
));
}
FileState::Link(_) if f.mode.is_some() => {
return Err(Mistake::new(Code::Conflict, "a link has no mode of its own").at("mode"));
}
_ => {}
}
Ok(FileStep {
path: f.path,
state,
owner: f.owner,
group: f.group,
mode: f.mode,
})
}
}
fn state(word: Option<StateWord>, link: Option<AbsPath>, url: Option<HttpsUrl>, sha256: Option<Sha256>) -> Result<FileState, Mistake> {
let given = usize::from(word.is_some()) + usize::from(link.is_some()) + usize::from(url.is_some());
if given > 1 {
return Err(Mistake::new(Code::Conflict, "a file step does one thing: one of `state`, `link` and `url`"));
}
match (word, link, url, sha256) {
(_, _, Some(_), None) => Err(
Mistake::new(Code::MissingField, "every fetch is pinned: a download needs the SHA-256 of what it fetches")
.at("url")
.fix("`sha256: <hex>` (`sha256sum <file>` prints it)"),
),
(_, _, None, Some(_)) => Err(Mistake::new(Code::InvalidValue, "`sha256` checks a download: it goes with `url`").at("sha256")),
(_, _, Some(url), Some(sha256)) => Ok(FileState::Download { url, sha256 }),
(Some(StateWord::Directory), ..) => Ok(FileState::Directory),
(Some(StateWord::Absent), ..) => Ok(FileState::Absent),
(None, Some(target), ..) => Ok(FileState::Link(target)),
(None, None, None, None) => Err(Mistake::new(Code::MissingField, "a file step says what to make of its `path`")
.fix("`state: directory`, `state: absent`, `link: <target>`, or `url` with `sha256`")),
}
}