github_actions/
command.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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()
        );
    }
}