github-actions 0.0.2

Utilities for developing custom GitHub Actions
Documentation
use std::fmt::Display;

pub struct Property<'a>(pub &'a str, pub &'a str);

impl<'a> Display for Property<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{}={}", self.0, escape_property(self.1)))
    }
}

pub struct Properties<'a>(pub Vec<Property<'a>>);

impl<'a> From<Vec<(&'a str, &'a str)>> for Properties<'a> {
    fn from(value: Vec<(&'a str, &'a str)>) -> Self {
        Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
    }
}

impl<'a> From<&[(&'a str, &'a str)]> for Properties<'a> {
    fn from(value: &[(&'a str, &'a str)]) -> Self {
        Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
    }
}

impl<'a, const N: usize> From<[(&'a str, &'a str); N]> for Properties<'a> {
    fn from(value: [(&'a str, &'a str); N]) -> Self {
        Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
    }
}

impl<'a> Display for Properties<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(
            self.0
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<_>>()
                .join(",")
                .as_str(),
        )
    }
}

pub struct Command<'a> {
    pub command: &'a str,
    pub value: &'a str,
    pub properties: Option<Properties<'a>>,
}

impl<'a> Display for Command<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.properties {
            Some(properties) => f.write_fmt(format_args!(
                "::{} {}::{}",
                self.command,
                properties.to_string(),
                escape_data(self.value)
            )),
            None => f.write_fmt(format_args!(
                "::{}::{}",
                self.command,
                escape_data(self.value)
            )),
        }
    }
}

#[derive(Default)]
pub struct CommandWithProperties<'a> {
    pub command: &'a str,
    pub value: &'a str,
    pub title: Option<&'a str>,
    pub file: Option<&'a str>,
    pub col: Option<usize>,
    pub end_column: Option<usize>,
    pub line: Option<usize>,
    pub end_line: Option<usize>,
}

impl<'a> Display for CommandWithProperties<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let col = self.col.map(|v| v.to_string());
        let end_column = self.end_column.map(|v| v.to_string());
        let line = self.line.map(|v| v.to_string());
        let end_line = self.line.map(|v| v.to_string());
        let params: Vec<(&str, &str)> = vec![
            ("title", self.title),
            ("file", self.file),
            ("col", col.as_deref()),
            ("endColumn", end_column.as_deref()),
            ("line", line.as_deref()),
            ("endLine", end_line.as_deref()),
        ]
        .into_iter()
        .filter_map(|(k, v)| match v {
            Some(v) => Some((k, v)),
            None => None,
        })
        .collect();

        Command {
            command: &self.command,
            value: &self.value,
            properties: Some(params.into()),
        }
        .fmt(f)
    }
}

pub fn escape_data<T: AsRef<str>>(s: T) -> String {
    s.as_ref()
        .replace('%', "%25")
        .replace('\r', "%0D")
        .replace('\n', "%0A")
}

pub fn escape_property<T: AsRef<str>>(s: T) -> String {
    s.as_ref()
        .replace('%', "%25")
        .replace('\r', "%0D")
        .replace('\n', "%0A")
        .replace(':', "%3A")
        .replace(',', "%2C")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_message() {
        let message = Command {
            command: "command",
            properties: None,
            value: "some message",
        };

        assert_eq!("::command::some message", message.to_string());
    }

    #[test]
    fn test_message_with_property() {
        let message = Command {
            command: "command",
            properties: Some(Properties(vec![Property("title", "value")])),
            value: "some message",
        };

        assert_eq!("::command title=value::some message", message.to_string());
    }

    #[test]
    fn test_message_with_properties() {
        let message = Command {
            command: "command",
            properties: Some(Properties(vec![
                Property("title", "value"),
                Property("line", "1"),
            ])),
            value: "some message",
        };

        assert_eq!(
            "::command title=value,line=1::some message",
            message.to_string()
        );
    }
}