isoloom_spec/syntax/step/
files.rs1use schemars::JsonSchema;
6use serde::Deserialize;
7
8use super::super::files::Mode;
9use super::super::mistake::Mistake;
10use super::super::problem::Code;
11use super::words::{AbsPath, GroupName, HttpsUrl, ProjectPath, Sha256, UserName};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
15#[serde(deny_unknown_fields)]
16pub struct Copy {
17 pub from: ProjectPath,
19 pub to: AbsPath,
21 #[serde(default)]
23 pub owner: Option<UserName>,
24 #[serde(default)]
26 pub mode: Option<Mode>,
27}
28
29impl Copy {
30 #[must_use]
32 pub fn is_folder(&self) -> bool {
33 self.from.as_str().ends_with('/')
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
40#[serde(try_from = "TemplateInput")]
41pub struct Template {
42 pub from: ProjectPath,
44 pub to: AbsPath,
46 pub owner: Option<UserName>,
48 pub group: Option<GroupName>,
50 pub mode: Option<Mode>,
52}
53
54schema_as!(Template, TemplateInput);
55
56#[derive(Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59struct TemplateInput {
60 from: ProjectPath,
64 to: AbsPath,
66 #[serde(default)]
68 owner: Option<UserName>,
69 #[serde(default)]
71 group: Option<GroupName>,
72 #[serde(default)]
74 mode: Option<Mode>,
75}
76
77impl TryFrom<TemplateInput> for Template {
78 type Error = Mistake;
79
80 fn try_from(t: TemplateInput) -> Result<Template, Mistake> {
81 if t.from.as_str().ends_with('/') {
82 return Err(Mistake::new(Code::InvalidValue, format!("`{}`: a template is one file", t.from))
83 .at("from")
84 .fix("a `copy` step copies a folder"));
85 }
86 Ok(Template {
87 from: t.from,
88 to: t.to,
89 owner: t.owner,
90 group: t.group,
91 mode: t.mode,
92 })
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
98#[serde(try_from = "FileInput")]
99pub struct FileStep {
100 pub path: AbsPath,
102 pub state: FileState,
104 pub owner: Option<UserName>,
106 pub group: Option<GroupName>,
108 pub mode: Option<Mode>,
110}
111
112schema_as!(FileStep, FileInput);
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116#[non_exhaustive]
117pub enum FileState {
118 Directory,
120 Absent,
122 Link(AbsPath),
124 Download {
126 url: HttpsUrl,
128 sha256: Sha256,
130 },
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
135#[serde(rename_all = "lowercase")]
136enum StateWord {
137 Directory,
139 Absent,
141}
142
143#[derive(Deserialize, JsonSchema)]
145#[serde(deny_unknown_fields)]
146struct FileInput {
147 path: AbsPath,
149 #[serde(default)]
152 state: Option<StateWord>,
153 #[serde(default)]
155 link: Option<AbsPath>,
156 #[serde(default)]
158 url: Option<HttpsUrl>,
159 #[serde(default)]
161 sha256: Option<Sha256>,
162 #[serde(default)]
164 owner: Option<UserName>,
165 #[serde(default)]
167 group: Option<GroupName>,
168 #[serde(default)]
170 mode: Option<Mode>,
171}
172
173impl TryFrom<FileInput> for FileStep {
174 type Error = Mistake;
175
176 fn try_from(f: FileInput) -> Result<FileStep, Mistake> {
177 let state = state(f.state, f.link, f.url, f.sha256)?;
178 let owned = f.owner.is_some() || f.group.is_some() || f.mode.is_some();
179 match state {
180 FileState::Absent if owned => {
181 return Err(Mistake::new(
182 Code::Conflict,
183 "`state: absent` removes the path, so it has no owner, group or mode",
184 ));
185 }
186 FileState::Link(_) if f.mode.is_some() => {
187 return Err(Mistake::new(Code::Conflict, "a link has no mode of its own").at("mode"));
188 }
189 _ => {}
190 }
191 Ok(FileStep {
192 path: f.path,
193 state,
194 owner: f.owner,
195 group: f.group,
196 mode: f.mode,
197 })
198 }
199}
200
201fn state(word: Option<StateWord>, link: Option<AbsPath>, url: Option<HttpsUrl>, sha256: Option<Sha256>) -> Result<FileState, Mistake> {
203 let given = usize::from(word.is_some()) + usize::from(link.is_some()) + usize::from(url.is_some());
204 if given > 1 {
205 return Err(Mistake::new(Code::Conflict, "a file step does one thing: one of `state`, `link` and `url`"));
206 }
207 match (word, link, url, sha256) {
208 (_, _, Some(_), None) => Err(
209 Mistake::new(Code::MissingField, "every fetch is pinned: a download needs the SHA-256 of what it fetches")
210 .at("url")
211 .fix("`sha256: <hex>` (`sha256sum <file>` prints it)"),
212 ),
213 (_, _, None, Some(_)) => Err(Mistake::new(Code::InvalidValue, "`sha256` checks a download: it goes with `url`").at("sha256")),
214 (_, _, Some(url), Some(sha256)) => Ok(FileState::Download { url, sha256 }),
215 (Some(StateWord::Directory), ..) => Ok(FileState::Directory),
216 (Some(StateWord::Absent), ..) => Ok(FileState::Absent),
217 (None, Some(target), ..) => Ok(FileState::Link(target)),
218 (None, None, None, None) => Err(Mistake::new(Code::MissingField, "a file step says what to make of its `path`")
219 .fix("`state: directory`, `state: absent`, `link: <target>`, or `url` with `sha256`")),
220 }
221}