aion_package/declared_command/template.rs
1//! The template shapes an emitted command is made of, and the values a
2//! caller binds into them.
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::error::RenderError;
8
9/// One piece of a fill template.
10///
11/// A hole names a declared parameter and nothing else: there is no
12/// expression, no default written inline, and no escape that could make a
13/// value decide the shape around it.
14#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum FillPiece {
17 /// Literal text, used exactly as written.
18 Literal {
19 /// The text itself.
20 text: String,
21 },
22 /// A named parameter's value, substituted whole.
23 Hole {
24 /// The parameter this hole reads.
25 parameter: String,
26 },
27}
28
29/// A fill template: literal pieces and named holes, in order.
30///
31/// Concatenating the pieces produces ONE string. The single exception is a
32/// template that is exactly one hole bound to a list, which becomes one argv
33/// element per item — see [`super::DeclaredCommandContract::render`].
34#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub struct FillTemplate {
36 /// The pieces, in order.
37 pub pieces: Vec<FillPiece>,
38}
39
40impl FillTemplate {
41 /// A template of one literal piece.
42 #[must_use]
43 pub fn literal(text: impl Into<String>) -> Self {
44 Self {
45 pieces: vec![FillPiece::Literal { text: text.into() }],
46 }
47 }
48
49 /// The parameter this template reads, when it is exactly one hole and
50 /// nothing else.
51 ///
52 /// This is the only shape a list may fill: a list written inside a larger
53 /// word has no single rendering, which is why the AWL checker refuses one
54 /// before it can reach here.
55 #[must_use]
56 pub fn sole_hole(&self) -> Option<&str> {
57 match self.pieces.as_slice() {
58 [FillPiece::Hole { parameter }] => Some(parameter.as_str()),
59 _ => None,
60 }
61 }
62
63 /// Every parameter this template reads, in order of appearance.
64 #[must_use]
65 pub fn holes(&self) -> Vec<&str> {
66 self.pieces
67 .iter()
68 .filter_map(|piece| match piece {
69 FillPiece::Literal { .. } => None,
70 FillPiece::Hole { parameter } => Some(parameter.as_str()),
71 })
72 .collect()
73 }
74}
75
76/// One value supplied for a declared parameter.
77///
78/// Two shapes, because a command's parameters have two shapes: a single value
79/// becomes one argv element, and a list becomes one element per item in the
80/// operand that carries it. Nothing here joins a list into a string — joining
81/// exists only to survive a re-split, and no re-split happens.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum ArgumentValue {
84 /// A single value, rendered as written.
85 Scalar(String),
86 /// A list of values, one argv element each.
87 List(Vec<String>),
88}
89
90impl ArgumentValue {
91 /// A scalar from anything string-like.
92 pub fn scalar(value: impl Into<String>) -> Self {
93 Self::Scalar(value.into())
94 }
95
96 /// A list from an iterator of string-likes.
97 pub fn list<I, S>(values: I) -> Self
98 where
99 I: IntoIterator<Item = S>,
100 S: Into<String>,
101 {
102 Self::List(values.into_iter().map(Into::into).collect())
103 }
104
105 /// Whether this value is a list.
106 #[must_use]
107 pub const fn is_list(&self) -> bool {
108 matches!(self, Self::List(_))
109 }
110
111 /// How a diagnostic names this value, without pretending a list is a
112 /// string.
113 #[must_use]
114 pub fn describe(&self) -> String {
115 match self {
116 Self::Scalar(text) => text.clone(),
117 Self::List(items) => format!("[{}]", items.join(", ")),
118 }
119 }
120
121 /// The argument value a JSON dispatch input carries for `parameter`.
122 ///
123 /// Strings, numbers and booleans have one obvious argument spelling and
124 /// take it. An array becomes a list, provided every item is itself one of
125 /// those three. `null` and an object have no single obvious spelling —
126 /// guessing one (empty string? JSON text? space-joined?) would make the
127 /// command mean something the author never wrote — so each is refused by
128 /// name.
129 ///
130 /// # Errors
131 ///
132 /// Returns [`RenderError::UnrepresentableValue`] for a value with no
133 /// unambiguous argument form, and [`RenderError::InteriorNul`] for a
134 /// string carrying a NUL byte, which the kernel would silently truncate
135 /// the argument at.
136 pub fn from_json(parameter: &str, value: &Value) -> Result<Self, RenderError> {
137 if let Value::Array(items) = value {
138 let mut rendered = Vec::with_capacity(items.len());
139 for item in items {
140 rendered.push(scalar_text(parameter, item)?);
141 }
142 return Ok(Self::List(rendered));
143 }
144 Ok(Self::Scalar(scalar_text(parameter, value)?))
145 }
146}
147
148/// One JSON value's argument text, or the refusal that it has none.
149fn scalar_text(parameter: &str, value: &Value) -> Result<String, RenderError> {
150 let unrepresentable = |kind: &'static str| RenderError::UnrepresentableValue {
151 parameter: parameter.to_owned(),
152 kind,
153 };
154 match value {
155 Value::String(text) => {
156 if text.contains('\0') {
157 return Err(RenderError::InteriorNul {
158 parameter: parameter.to_owned(),
159 });
160 }
161 Ok(text.clone())
162 }
163 Value::Number(number) => Ok(number.to_string()),
164 Value::Bool(flag) => Ok(if *flag { "true" } else { "false" }.to_owned()),
165 Value::Null => Err(unrepresentable("null")),
166 Value::Array(_) => Err(unrepresentable("a nested array")),
167 Value::Object(_) => Err(unrepresentable("an object")),
168 }
169}