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, which is ONE whole argv
32/// element — always, with no exception. A value carrying spaces is one
33/// element containing spaces, and no bound value can turn one slot into two
34/// arguments, because nothing here re-splits or splats anything.
35#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub struct FillTemplate {
37 /// The pieces, in order.
38 pub pieces: Vec<FillPiece>,
39}
40
41impl FillTemplate {
42 /// A template of one literal piece.
43 #[must_use]
44 pub fn literal(text: impl Into<String>) -> Self {
45 Self {
46 pieces: vec![FillPiece::Literal { text: text.into() }],
47 }
48 }
49
50 /// Every parameter this template reads, in order of appearance.
51 #[must_use]
52 pub fn holes(&self) -> Vec<&str> {
53 self.pieces
54 .iter()
55 .filter_map(|piece| match piece {
56 FillPiece::Literal { .. } => None,
57 FillPiece::Hole { parameter } => Some(parameter.as_str()),
58 })
59 .collect()
60 }
61}
62
63/// One value supplied for a declared parameter.
64///
65/// Two shapes, because a caller can SUPPLY two shapes — but only one of them
66/// renders. A command parameter is one command-line word, so a single value
67/// becomes that word; a list has no rendering at all and is refused by name,
68/// with both shapes stated, before any argv exists
69/// ([`super::DeclaredCommandContract::render`]). The list shape is carried
70/// here precisely so that refusal can see it and say what arrived, rather
71/// than joining it into a string nobody wrote.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum ArgumentValue {
74 /// A single value, rendered as written.
75 Scalar(String),
76 /// Several values supplied at once, which no declared parameter accepts:
77 /// carried so the refusal can name the shape that arrived.
78 List(Vec<String>),
79}
80
81impl ArgumentValue {
82 /// A scalar from anything string-like.
83 pub fn scalar(value: impl Into<String>) -> Self {
84 Self::Scalar(value.into())
85 }
86
87 /// A list from an iterator of string-likes.
88 pub fn list<I, S>(values: I) -> Self
89 where
90 I: IntoIterator<Item = S>,
91 S: Into<String>,
92 {
93 Self::List(values.into_iter().map(Into::into).collect())
94 }
95
96 /// How a diagnostic names this value, without pretending a list is a
97 /// string.
98 #[must_use]
99 pub fn describe(&self) -> String {
100 match self {
101 Self::Scalar(text) => text.clone(),
102 Self::List(items) => format!("[{}]", items.join(", ")),
103 }
104 }
105
106 /// The argument value a JSON dispatch input carries for `parameter`.
107 ///
108 /// Strings, numbers and booleans have one obvious argument spelling and
109 /// take it. An array becomes a list, provided every item is itself one of
110 /// those three. `null` and an object have no single obvious spelling —
111 /// guessing one (empty string? JSON text? space-joined?) would make the
112 /// command mean something the author never wrote — so each is refused by
113 /// name.
114 ///
115 /// # Errors
116 ///
117 /// Returns [`RenderError::UnrepresentableValue`] for a value with no
118 /// unambiguous argument form, and [`RenderError::InteriorNul`] for a
119 /// string carrying a NUL byte, which the kernel would silently truncate
120 /// the argument at.
121 pub fn from_json(parameter: &str, value: &Value) -> Result<Self, RenderError> {
122 if let Value::Array(items) = value {
123 let mut rendered = Vec::with_capacity(items.len());
124 for item in items {
125 rendered.push(scalar_text(parameter, item)?);
126 }
127 return Ok(Self::List(rendered));
128 }
129 Ok(Self::Scalar(scalar_text(parameter, value)?))
130 }
131}
132
133/// One JSON value's argument text, or the refusal that it has none.
134fn scalar_text(parameter: &str, value: &Value) -> Result<String, RenderError> {
135 let unrepresentable = |kind: &'static str| RenderError::UnrepresentableValue {
136 parameter: parameter.to_owned(),
137 kind,
138 };
139 match value {
140 Value::String(text) => {
141 if text.contains('\0') {
142 return Err(RenderError::InteriorNul {
143 parameter: parameter.to_owned(),
144 });
145 }
146 Ok(text.clone())
147 }
148 Value::Number(number) => Ok(number.to_string()),
149 Value::Bool(flag) => Ok(if *flag { "true" } else { "false" }.to_owned()),
150 Value::Null => Err(unrepresentable("null")),
151 Value::Array(_) => Err(unrepresentable("a nested array")),
152 Value::Object(_) => Err(unrepresentable("an object")),
153 }
154}