aion-package 0.30.0

Archive validation, content hashing, and namespacing for Aion workflow packages.
Documentation
//! The template shapes an emitted command is made of, and the values a
//! caller binds into them.

use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::error::RenderError;

/// One piece of a fill template.
///
/// A hole names a declared parameter and nothing else: there is no
/// expression, no default written inline, and no escape that could make a
/// value decide the shape around it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FillPiece {
    /// Literal text, used exactly as written.
    Literal {
        /// The text itself.
        text: String,
    },
    /// A named parameter's value, substituted whole.
    Hole {
        /// The parameter this hole reads.
        parameter: String,
    },
}

/// A fill template: literal pieces and named holes, in order.
///
/// Concatenating the pieces produces ONE string, which is ONE whole argv
/// element — always, with no exception. A value carrying spaces is one
/// element containing spaces, and no bound value can turn one slot into two
/// arguments, because nothing here re-splits or splats anything.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FillTemplate {
    /// The pieces, in order.
    pub pieces: Vec<FillPiece>,
}

impl FillTemplate {
    /// A template of one literal piece.
    #[must_use]
    pub fn literal(text: impl Into<String>) -> Self {
        Self {
            pieces: vec![FillPiece::Literal { text: text.into() }],
        }
    }

    /// Every parameter this template reads, in order of appearance.
    #[must_use]
    pub fn holes(&self) -> Vec<&str> {
        self.pieces
            .iter()
            .filter_map(|piece| match piece {
                FillPiece::Literal { .. } => None,
                FillPiece::Hole { parameter } => Some(parameter.as_str()),
            })
            .collect()
    }
}

/// One value supplied for a declared parameter.
///
/// Two shapes, because a caller can SUPPLY two shapes — but only one of them
/// renders. A command parameter is one command-line word, so a single value
/// becomes that word; a list has no rendering at all and is refused by name,
/// with both shapes stated, before any argv exists
/// ([`super::DeclaredCommandContract::render`]). The list shape is carried
/// here precisely so that refusal can see it and say what arrived, rather
/// than joining it into a string nobody wrote.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ArgumentValue {
    /// A single value, rendered as written.
    Scalar(String),
    /// Several values supplied at once, which no declared parameter accepts:
    /// carried so the refusal can name the shape that arrived.
    List(Vec<String>),
}

impl ArgumentValue {
    /// A scalar from anything string-like.
    pub fn scalar(value: impl Into<String>) -> Self {
        Self::Scalar(value.into())
    }

    /// A list from an iterator of string-likes.
    pub fn list<I, S>(values: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self::List(values.into_iter().map(Into::into).collect())
    }

    /// How a diagnostic names this value, without pretending a list is a
    /// string.
    #[must_use]
    pub fn describe(&self) -> String {
        match self {
            Self::Scalar(text) => text.clone(),
            Self::List(items) => format!("[{}]", items.join(", ")),
        }
    }

    /// The argument value a JSON dispatch input carries for `parameter`.
    ///
    /// Strings, numbers and booleans have one obvious argument spelling and
    /// take it. An array becomes a list, provided every item is itself one of
    /// those three. `null` and an object have no single obvious spelling —
    /// guessing one (empty string? JSON text? space-joined?) would make the
    /// command mean something the author never wrote — so each is refused by
    /// name.
    ///
    /// # Errors
    ///
    /// Returns [`RenderError::UnrepresentableValue`] for a value with no
    /// unambiguous argument form, and [`RenderError::InteriorNul`] for a
    /// string carrying a NUL byte, which the kernel would silently truncate
    /// the argument at.
    pub fn from_json(parameter: &str, value: &Value) -> Result<Self, RenderError> {
        if let Value::Array(items) = value {
            let mut rendered = Vec::with_capacity(items.len());
            for item in items {
                rendered.push(scalar_text(parameter, item)?);
            }
            return Ok(Self::List(rendered));
        }
        Ok(Self::Scalar(scalar_text(parameter, value)?))
    }
}

/// One JSON value's argument text, or the refusal that it has none.
fn scalar_text(parameter: &str, value: &Value) -> Result<String, RenderError> {
    let unrepresentable = |kind: &'static str| RenderError::UnrepresentableValue {
        parameter: parameter.to_owned(),
        kind,
    };
    match value {
        Value::String(text) => {
            if text.contains('\0') {
                return Err(RenderError::InteriorNul {
                    parameter: parameter.to_owned(),
                });
            }
            Ok(text.clone())
        }
        Value::Number(number) => Ok(number.to_string()),
        Value::Bool(flag) => Ok(if *flag { "true" } else { "false" }.to_owned()),
        Value::Null => Err(unrepresentable("null")),
        Value::Array(_) => Err(unrepresentable("a nested array")),
        Value::Object(_) => Err(unrepresentable("an object")),
    }
}