Skip to main content

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}