github_actions/
command.rs

1use std::fmt::Display;
2
3pub struct Property<'a>(pub &'a str, pub &'a str);
4
5impl<'a> Display for Property<'a> {
6    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7        f.write_fmt(format_args!("{}={}", self.0, escape_property(self.1)))
8    }
9}
10
11pub struct Properties<'a>(pub Vec<Property<'a>>);
12
13impl<'a> From<Vec<(&'a str, &'a str)>> for Properties<'a> {
14    fn from(value: Vec<(&'a str, &'a str)>) -> Self {
15        Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
16    }
17}
18
19impl<'a> From<&[(&'a str, &'a str)]> for Properties<'a> {
20    fn from(value: &[(&'a str, &'a str)]) -> Self {
21        Properties(value.iter().map(|(k, v)| Property(k, v)).collect())
22    }
23}
24
25impl<'a, const N: usize> From<[(&'a str, &'a str); N]> for Properties<'a> {
26    fn from(value: [(&'a str, &'a str); N]) -> Self {
27        Properties(value.into_iter().map(|(k, v)| Property(k, v)).collect())
28    }
29}
30
31impl<'a> Display for Properties<'a> {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.write_str(
34            self.0
35                .iter()
36                .map(|v| v.to_string())
37                .collect::<Vec<_>>()
38                .join(",")
39                .as_str(),
40        )
41    }
42}
43
44pub struct Command<'a> {
45    pub command: &'a str,
46    pub value: &'a str,
47    pub properties: Option<Properties<'a>>,
48}
49
50impl<'a> Display for Command<'a> {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match &self.properties {
53            Some(properties) => f.write_fmt(format_args!(
54                "::{} {}::{}",
55                self.command,
56                properties,
57                escape_data(self.value)
58            )),
59            None => f.write_fmt(format_args!(
60                "::{}::{}",
61                self.command,
62                escape_data(self.value)
63            )),
64        }
65    }
66}
67
68#[derive(Default)]
69pub struct CommandWithProperties<'a> {
70    pub command: &'a str,
71    pub value: &'a str,
72    pub title: Option<&'a str>,
73    pub file: Option<&'a str>,
74    pub col: Option<usize>,
75    pub end_column: Option<usize>,
76    pub line: Option<usize>,
77    pub end_line: Option<usize>,
78}
79
80impl<'a> Display for CommandWithProperties<'a> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        let col = self.col.map(|v| v.to_string());
83        let end_column = self.end_column.map(|v| v.to_string());
84        let line = self.line.map(|v| v.to_string());
85        let end_line = self.line.map(|v| v.to_string());
86        let params: Vec<(&str, &str)> = vec![
87            ("title", self.title),
88            ("file", self.file),
89            ("col", col.as_deref()),
90            ("endColumn", end_column.as_deref()),
91            ("line", line.as_deref()),
92            ("endLine", end_line.as_deref()),
93        ]
94        .into_iter()
95        .filter_map(|(k, v)| v.map(|v| (k, v)))
96        .collect();
97
98        Command {
99            command: self.command,
100            value: self.value,
101            properties: Some(params.into()),
102        }
103        .fmt(f)
104    }
105}
106
107pub fn escape_data<T: AsRef<str>>(s: T) -> String {
108    s.as_ref()
109        .replace('%', "%25")
110        .replace('\r', "%0D")
111        .replace('\n', "%0A")
112}
113
114pub fn escape_property<T: AsRef<str>>(s: T) -> String {
115    s.as_ref()
116        .replace('%', "%25")
117        .replace('\r', "%0D")
118        .replace('\n', "%0A")
119        .replace(':', "%3A")
120        .replace(',', "%2C")
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_message() {
129        let message = Command {
130            command: "command",
131            properties: None,
132            value: "some message",
133        };
134
135        assert_eq!("::command::some message", message.to_string());
136    }
137
138    #[test]
139    fn test_message_with_property() {
140        let message = Command {
141            command: "command",
142            properties: Some(Properties(vec![Property("title", "value")])),
143            value: "some message",
144        };
145
146        assert_eq!("::command title=value::some message", message.to_string());
147    }
148
149    #[test]
150    fn test_message_with_properties() {
151        let message = Command {
152            command: "command",
153            properties: Some(Properties(vec![
154                Property("title", "value"),
155                Property("line", "1"),
156            ])),
157            value: "some message",
158        };
159
160        assert_eq!(
161            "::command title=value,line=1::some message",
162            message.to_string()
163        );
164    }
165}