isoloom_spec/syntax/step/
services.rs1use std::borrow::Cow;
6use std::fmt;
7use std::num::NonZeroU16;
8
9use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
10use serde::de::{self, MapAccess, SeqAccess, Visitor};
11use serde::{Deserialize, Deserializer};
12
13use super::super::mistake::Mistake;
14use super::super::names::PortName;
15use super::super::problem::Code;
16use super::words::{AbsPath, UnitName, UserName};
17use crate::model::Protocol;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct Argv(Vec<String>);
22
23impl Argv {
24 #[must_use]
26 pub fn args(&self) -> &[String] {
27 &self.0
28 }
29
30 #[must_use]
32 pub fn program(&self) -> &str {
33 self.0.first().map_or("", String::as_str)
34 }
35}
36
37impl TryFrom<Vec<String>> for Argv {
38 type Error = String;
39
40 fn try_from(args: Vec<String>) -> Result<Argv, String> {
46 match args.first() {
47 Some(program) if !program.trim().is_empty() => Ok(Argv(args)),
48 _ => Err("a command starts with its program (`[/usr/sbin/myd, --foreground]`)".to_owned()),
49 }
50 }
51}
52
53impl<'de> Deserialize<'de> for Argv {
54 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Argv, D::Error> {
55 Argv::try_from(Vec::<String>::deserialize(d)?).map_err(de::Error::custom)
56 }
57}
58
59impl JsonSchema for Argv {
60 fn schema_name() -> Cow<'static, str> {
61 "Argv".into()
62 }
63
64 fn inline_schema() -> bool {
65 true
66 }
67
68 fn json_schema(_: &mut SchemaGenerator) -> Schema {
69 json_schema!({ "type": "array", "items": { "type": "string" }, "minItems": 1 })
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76#[non_exhaustive]
77pub enum Invocation {
78 Shell(String),
80 Exec(Argv),
82}
83
84impl<'de> Deserialize<'de> for Invocation {
85 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Invocation, D::Error> {
86 d.deserialize_any(InvocationVisitor)
87 }
88}
89
90struct InvocationVisitor;
92
93impl<'de> Visitor<'de> for InvocationVisitor {
94 type Value = Invocation;
95
96 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
97 f.write_str("a text the shell runs, or a list of arguments")
98 }
99
100 fn visit_str<E: de::Error>(self, v: &str) -> Result<Invocation, E> {
101 if v.trim().is_empty() {
102 return Err(E::custom("a command says what it runs"));
103 }
104 Ok(Invocation::Shell(v.to_owned()))
105 }
106
107 fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Invocation, A::Error> {
108 Argv::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(Invocation::Exec)
109 }
110}
111
112impl JsonSchema for Invocation {
113 fn schema_name() -> Cow<'static, str> {
114 "Invocation".into()
115 }
116
117 fn inline_schema() -> bool {
118 true
119 }
120
121 fn json_schema(_: &mut SchemaGenerator) -> Schema {
122 json_schema!({ "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "items": { "type": "string" }, "minItems": 1 }] })
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128#[non_exhaustive]
129pub enum Guard {
130 Creates(AbsPath),
132 Unless(Invocation),
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138pub struct CommandStep {
139 pub run: Invocation,
141 pub guard: Guard,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
148#[serde(try_from = "ServiceInput")]
149#[non_exhaustive]
150pub enum Service {
151 Unit {
153 name: UnitName,
155 enabled: bool,
157 },
158 Defined {
161 name: UnitName,
163 run: Argv,
165 user: Option<UserName>,
167 cwd: Option<AbsPath>,
169 enabled: bool,
171 },
172}
173
174impl Service {
175 #[must_use]
177 pub fn name(&self) -> &UnitName {
178 match self {
179 Service::Unit { name, .. } | Service::Defined { name, .. } => name,
180 }
181 }
182
183 #[must_use]
185 pub fn enabled(&self) -> bool {
186 match self {
187 Service::Unit { enabled, .. } | Service::Defined { enabled, .. } => *enabled,
188 }
189 }
190}
191
192#[derive(Deserialize, JsonSchema)]
194#[serde(deny_unknown_fields)]
195struct ServiceFull {
196 name: UnitName,
198 #[serde(default)]
201 run: Option<Argv>,
202 #[serde(default)]
204 user: Option<UserName>,
205 #[serde(default)]
207 cwd: Option<AbsPath>,
208 #[serde(default)]
210 enabled: Option<bool>,
211}
212
213#[derive(JsonSchema)]
216#[serde(untagged)]
217enum ServiceInput {
218 Unit(UnitName),
220 Full(ServiceFull),
222}
223
224schema_as!(Service, ServiceInput);
225
226impl<'de> Deserialize<'de> for ServiceInput {
227 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<ServiceInput, D::Error> {
228 struct V;
229 impl<'de> Visitor<'de> for V {
230 type Value = ServiceInput;
231 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
232 f.write_str("a unit's name, or `{ name, run, user, cwd, enabled }`")
233 }
234 fn visit_str<E: de::Error>(self, v: &str) -> Result<ServiceInput, E> {
235 v.parse().map(ServiceInput::Unit).map_err(E::custom)
236 }
237 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<ServiceInput, A::Error> {
238 ServiceFull::deserialize(de::value::MapAccessDeserializer::new(map)).map(ServiceInput::Full)
239 }
240 }
241 d.deserialize_any(V)
242 }
243}
244
245impl TryFrom<ServiceInput> for Service {
246 type Error = Mistake;
247
248 fn try_from(written: ServiceInput) -> Result<Service, Mistake> {
249 let s = match written {
250 ServiceInput::Unit(name) => return Ok(Service::Unit { name, enabled: true }),
251 ServiceInput::Full(s) => s,
252 };
253 let enabled = s.enabled.unwrap_or(true);
254 match s.run {
255 Some(run) => Ok(Service::Defined {
256 name: s.name,
257 run,
258 user: s.user,
259 cwd: s.cwd,
260 enabled,
261 }),
262 None if s.user.is_some() || s.cwd.is_some() => {
263 let field = if s.user.is_some() { "user" } else { "cwd" };
264 Err(
265 Mistake::new(Code::Conflict, format!("`{field}` goes with `run`: a package's unit runs as its package says"))
266 .at(field)
267 .fix("`run: [<program>, <args>...]` to define the service, or leave it out"),
268 )
269 }
270 None => Ok(Service::Unit { name: s.name, enabled }),
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, JsonSchema)]
277#[serde(deny_unknown_fields)]
278pub struct Firewall {
279 pub allow: Vec<FirewallPort>,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Hash)]
287#[non_exhaustive]
288pub enum FirewallPort {
289 Named(PortName),
291 Number {
293 port: NonZeroU16,
295 protocol: Protocol,
297 },
298}
299
300impl<'de> Deserialize<'de> for FirewallPort {
301 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<FirewallPort, D::Error> {
302 d.deserialize_any(PortVisitor)
303 }
304}
305
306struct PortVisitor;
308
309impl Visitor<'_> for PortVisitor {
310 type Value = FirewallPort;
311
312 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
313 f.write_str("a port's name, a number, or `<number>/udp`")
314 }
315
316 fn visit_u64<E: de::Error>(self, v: u64) -> Result<FirewallPort, E> {
317 number(&v.to_string(), Protocol::Tcp)
318 }
319
320 fn visit_i64<E: de::Error>(self, v: i64) -> Result<FirewallPort, E> {
321 number(&v.to_string(), Protocol::Tcp)
322 }
323
324 fn visit_str<E: de::Error>(self, v: &str) -> Result<FirewallPort, E> {
325 match v.split_once('/') {
326 Some((n, "udp")) => number(n, Protocol::Udp),
327 Some((n, "tcp")) => number(n, Protocol::Tcp),
328 Some(_) => Err(E::custom(format!("`{v}`: a port is `<number>`, `<number>/udp` or `<number>/tcp`"))),
329 None if v.starts_with(|c: char| c.is_ascii_digit()) => number(v, Protocol::Tcp),
330 None => v.parse().map(FirewallPort::Named).map_err(E::custom),
331 }
332 }
333}
334
335fn number<E: de::Error>(n: &str, protocol: Protocol) -> Result<FirewallPort, E> {
337 n.parse::<NonZeroU16>()
338 .map(|port| FirewallPort::Number { port, protocol })
339 .map_err(|_| E::custom(format!("`{n}` isn't a port: 1 to 65535")))
340}
341
342impl JsonSchema for FirewallPort {
343 fn schema_name() -> Cow<'static, str> {
344 "FirewallPort".into()
345 }
346
347 fn inline_schema() -> bool {
348 true
349 }
350
351 fn json_schema(_: &mut SchemaGenerator) -> Schema {
352 json_schema!({ "anyOf": [
353 { "type": "integer", "minimum": 1, "maximum": 65535 },
354 { "type": "string", "pattern": "^([a-z][a-z0-9-]{0,30}|[0-9]{1,5}(/(udp|tcp))?)$" },
355 ] })
356 }
357}